Merge "Fix app crash when service produces a null route provider" into androidx-main
diff --git a/activity/settings.gradle b/activity/settings.gradle
index 34492d9..91aede1 100644
--- a/activity/settings.gradle
+++ b/activity/settings.gradle
@@ -28,7 +28,7 @@
setupPlayground("..")
selectProjectsFromAndroidX({ name ->
if (name.startsWith(":activity")) return true
- if (name.startsWith(":lifecycle")) return true
+ if (name.startsWith(":lifecycle") && !name.contains("integration-tests")) return true
if (name.startsWith(":annotation")) return true
if (name == ":internal-testutils-runtime") return true
if (name == ":internal-testutils-truth") return true
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/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 8784f69..94141ef 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -34,6 +34,8 @@
import com.google.gson.GsonBuilder
import java.io.File
import java.io.FileNotFoundException
+import java.time.Duration
+import java.time.LocalDateTime
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
@@ -435,7 +437,12 @@
task.destinationFile.set(getMetadataRegularFile(project))
}
+ val metricsDirectory = project.buildDir
+ val metricsFile = File(metricsDirectory, "build-metrics.json")
+ val projectName = project.name
+
val dackkaTask = project.tasks.register("docs", DackkaTask::class.java) { task ->
+ var taskStartTime: LocalDateTime? = null
task.apply {
dependsOn(unzipJvmSourcesTask)
dependsOn(unzipSamplesTask)
@@ -462,6 +469,17 @@
// See go/dackka-source-link for details on this link.
baseSourceLink = "https://cs.android.com/search?" +
"q=file:%s+class:%s&ss=androidx/platform/frameworks/support"
+ task.doFirst {
+ taskStartTime = LocalDateTime.now()
+ }
+ task.doLast {
+ val taskEndTime = LocalDateTime.now()
+ val duration = Duration.between(taskStartTime, taskEndTime).toMillis()
+ metricsDirectory.mkdirs()
+ metricsFile.writeText(
+ "{ \"${projectName}_docs_execution_duration\": $duration }"
+ )
+ }
}
}
diff --git a/busytown/androidx.sh b/busytown/androidx.sh
index 52fb12e..2e0f9e9 100755
--- a/busytown/androidx.sh
+++ b/busytown/androidx.sh
@@ -28,7 +28,7 @@
# Parse performance profile reports (generated with the --profile option above) and re-export
# the metrics in an easily machine-readable format for tracking
- impl/parse_profile_htmls.sh
+ impl/parse_profile_data.sh
fi
echo "Completing $0 at $(date) with exit value $EXIT_VALUE"
diff --git a/busytown/androidx_incremental.sh b/busytown/androidx_incremental.sh
index 4b17487..a9f5b1a 100755
--- a/busytown/androidx_incremental.sh
+++ b/busytown/androidx_incremental.sh
@@ -66,7 +66,7 @@
fi
# Parse performance profile reports (generated with the --profile option above) and re-export the metrics in an easily machine-readable format for tracking
- impl/parse_profile_htmls.sh
+ impl/parse_profile_data.sh
fi
echo "Completing $0 at $(date) with exit value $EXIT_VALUE"
diff --git a/busytown/impl/build-studio-and-androidx.sh b/busytown/impl/build-studio-and-androidx.sh
index 2d87b18..0359b84 100755
--- a/busytown/impl/build-studio-and-androidx.sh
+++ b/busytown/impl/build-studio-and-androidx.sh
@@ -108,7 +108,7 @@
else
RETURN_CODE=1
fi
- $SCRIPTS_DIR/impl/parse_profile_htmls.sh
+ $SCRIPTS_DIR/impl/parse_profile_data.sh
# zip build scan
scanZip="$DIST_DIR/scan.zip"
diff --git a/busytown/impl/parse_profile_data.sh b/busytown/impl/parse_profile_data.sh
new file mode 100755
index 0000000..84f6844
--- /dev/null
+++ b/busytown/impl/parse_profile_data.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -e
+
+# This is a helper script to be called by androidx.sh
+# This script locates, parses, and merges build profiling information from various report files
+
+cd "$(dirname $0)"
+
+if [ "$OUT_DIR" == "" ]; then
+ OUT_DIR=../../../../out
+fi
+if [ "$DIST_DIR" == "" ]; then
+ DIST_DIR="$OUT_DIR/dist"
+fi
+
+METRICS_DIR="$DIST_DIR/librarymetrics/build"
+INTERMEDIATES_DIR=$OUT_DIR
+
+# Find the metrics files that exist
+METRICS_FILES="$(echo $OUT_DIR/androidx/*/build/build-metrics.json | grep -v '*' || true)"
+
+# Look for a profile file and attempt to make a metrics json out of it
+PROFILE_FILES="$OUT_DIR/androidx/build/reports/profile/*.html"
+if ls $PROFILE_FILES >/dev/null 2>&1 ; then
+ # parse the profile file and generate a .json file summarizing it
+ PROFILE_JSON=$INTERMEDIATES_DIR/build_androidx.json
+ ./parse_profile_html.py --input-profile "$(ls $PROFILE_FILES | sort | tail -n 2 | head -n 1)" --output-summary $PROFILE_JSON
+ METRICS_FILES="$METRICS_FILES $PROFILE_JSON"
+fi
+
+if [ "$METRICS_FILES" != "" ]; then
+ # merge all profiles
+ mkdir -p "$METRICS_DIR"
+ # concatenate files, and replace "}{" with ", ", ignoring whitespace
+ cat $METRICS_FILES | sed 's/ *} *{ */, /g' > $METRICS_DIR/build_androidx.json
+ # remove metrics files so that next time if Gradle skips emitting them then we don't get old results
+ rm -f $METRICS_FILES
+fi
diff --git a/busytown/impl/parse_profile_htmls.sh b/busytown/impl/parse_profile_htmls.sh
deleted file mode 100755
index 8c5e2b6..0000000
--- a/busytown/impl/parse_profile_htmls.sh
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/bin/bash
-set -e
-
-# This is a helper script to be called by androidx.sh
-# This script asks parse_profile_html.py to parse the appropriate report html files
-
-cd "$(dirname $0)"
-
-if [ "$OUT_DIR" == "" ]; then
- OUT_DIR=../../../../out
-fi
-if [ "$DIST_DIR" == "" ]; then
- DIST_DIR="$OUT_DIR/dist"
-fi
-
-METRICS_DIR="$DIST_DIR/librarymetrics/build"
-
-# If a profile file exists, parse it. If not, do nothing
-PROFILE_FILES="$OUT_DIR/androidx/build/reports/profile/*.html"
-if ls $PROFILE_FILES >/dev/null 2>&1 ; then
- ./parse_profile_html.py --input-profile "$(ls $PROFILE_FILES | sort | tail -n 2 | head -n 1)" --output-summary $METRICS_DIR/build_androidx.json
-fi
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/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
index 9ab3c3c..3c3ebe9 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -23,6 +23,7 @@
import android.hardware.camera2.params.MeteringRectangle
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
@@ -35,8 +36,10 @@
import dagger.Module
import javax.inject.Inject
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
internal val useCaseCameraIds = atomic(0)
@@ -85,7 +88,7 @@
override val requestControl: UseCaseCameraRequestControl,
) : UseCaseCamera {
private val debugId = useCaseCameraIds.incrementAndGet()
- private val graphStateJob: Job
+ private val closed = atomic(false)
override var runningUseCases = setOf<UseCase>()
set(value) {
@@ -109,21 +112,27 @@
useCaseGraphConfig.apply {
cameraStateAdapter.onGraphUpdated(graph)
}
- graphStateJob = threads.scope.launch {
+ threads.scope.launch {
useCaseGraphConfig.apply {
graph.graphState.collect {
cameraStateAdapter.onGraphStateUpdated(graph, it)
+ if (closed.value && it is GraphStateStopped) {
+ cancel()
+ }
}
}
}
}
override fun close(): Job {
- graphStateJob.cancel()
- return threads.scope.launch {
- debug { "Closing $this" }
- useCaseGraphConfig.graph.close()
- useCaseSurfaceManager.stopAsync().await()
+ return if (closed.compareAndSet(expect = false, update = true)) {
+ threads.scope.launch {
+ debug { "Closing $this" }
+ useCaseGraphConfig.graph.close()
+ useCaseSurfaceManager.stopAsync().await()
+ }
+ } else {
+ CompletableDeferred(Unit)
}
}
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/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
index c7ef572..053529d 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/compat/workaround/ExtraSupportedSurfaceCombinationsContainerDeviceTest.kt
@@ -21,8 +21,6 @@
import android.hardware.camera2.CameraCharacteristics
import android.os.Handler
import android.os.Looper
-import android.util.Size
-import android.view.Surface
import androidx.camera.camera2.Camera2Config
import androidx.camera.camera2.internal.Camera2CameraFactory
import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks
@@ -38,10 +36,8 @@
import androidx.camera.core.impl.CameraConfig
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraThreadConfig
-import androidx.camera.core.impl.CaptureProcessor
import androidx.camera.core.impl.Config
import androidx.camera.core.impl.Identifier
-import androidx.camera.core.impl.ImageProxyBundle
import androidx.camera.core.impl.MutableOptionsBundle
import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.SurfaceCombination
@@ -372,21 +368,4 @@
assertThat(latch.await(timeout, TimeUnit.MILLISECONDS)).isTrue()
}
}
-
- private class FakePreviewCaptureProcessor : CaptureProcessor {
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {
- // No-op
- }
-
- override fun process(bundle: ImageProxyBundle) {
- bundle.captureIds.forEach {
- val image = bundle.getImageProxy(it).get()
- image.close()
- }
- }
-
- override fun onResolutionUpdate(size: Size) {
- // No-op
- }
- }
}
\ No newline at end of file
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/api/current.txt b/camera/camera-core/api/current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -349,6 +358,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index 139d818..41025e7 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -362,6 +371,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -373,6 +386,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -410,12 +443,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 4eb4cb2..401ff46 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -23,6 +23,15 @@
public static final class CameraControl.OperationCanceledException extends java.lang.Exception {
}
+ @RequiresApi(21) public abstract class CameraEffect {
+ ctor protected CameraEffect(int, java.util.concurrent.Executor, androidx.camera.core.SurfaceProcessor);
+ method public java.util.concurrent.Executor getExecutor();
+ method public androidx.camera.core.SurfaceProcessor? getSurfaceProcessor();
+ method public int getTargets();
+ field public static final int IMAGE_CAPTURE = 4; // 0x4
+ field public static final int PREVIEW = 1; // 0x1
+ }
+
@RequiresApi(21) public interface CameraFilter {
method public java.util.List<androidx.camera.core.CameraInfo!> filter(java.util.List<androidx.camera.core.CameraInfo!>);
}
@@ -349,6 +358,10 @@
method public void onSurfaceRequested(androidx.camera.core.SurfaceRequest);
}
+ public class ProcessingException extends java.lang.Exception {
+ ctor public ProcessingException();
+ }
+
@RequiresApi(21) @com.google.auto.value.AutoValue public abstract class ResolutionInfo {
method public abstract android.graphics.Rect getCropRect();
method public abstract android.util.Size getResolution();
@@ -360,6 +373,26 @@
ctor public SurfaceOrientedMeteringPointFactory(float, float, androidx.camera.core.UseCase);
}
+ public interface SurfaceOutput {
+ method public void close();
+ method public android.util.Size getSize();
+ method public android.view.Surface getSurface(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.core.SurfaceOutput.Event!>);
+ method public int getTargets();
+ method public void updateTransformMatrix(float[], float[]);
+ }
+
+ @com.google.auto.value.AutoValue public abstract static class SurfaceOutput.Event {
+ ctor public SurfaceOutput.Event();
+ method public abstract int getEventCode();
+ method public abstract androidx.camera.core.SurfaceOutput getSurfaceOutput();
+ field public static final int EVENT_REQUEST_CLOSE = 0; // 0x0
+ }
+
+ public interface SurfaceProcessor {
+ method public void onInputSurface(androidx.camera.core.SurfaceRequest) throws androidx.camera.core.ProcessingException;
+ method public void onOutputSurface(androidx.camera.core.SurfaceOutput) throws androidx.camera.core.ProcessingException;
+ }
+
@RequiresApi(21) public final class SurfaceRequest {
method public void addRequestCancellationListener(java.util.concurrent.Executor, Runnable);
method public void clearTransformationInfoListener();
@@ -397,12 +430,14 @@
}
@RequiresApi(21) public final class UseCaseGroup {
+ method public java.util.List<androidx.camera.core.CameraEffect!> getEffects();
method public java.util.List<androidx.camera.core.UseCase!> getUseCases();
method public androidx.camera.core.ViewPort? getViewPort();
}
public static final class UseCaseGroup.Builder {
ctor public UseCaseGroup.Builder();
+ method public androidx.camera.core.UseCaseGroup.Builder addEffect(androidx.camera.core.CameraEffect);
method public androidx.camera.core.UseCaseGroup.Builder addUseCase(androidx.camera.core.UseCase);
method public androidx.camera.core.UseCaseGroup build();
method public androidx.camera.core.UseCaseGroup.Builder setViewPort(androidx.camera.core.ViewPort);
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt
deleted file mode 100644
index 7d1c8fd..0000000
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ProcessingImageReaderDeviceTest.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright 2020 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
-
-import android.graphics.ImageFormat
-import android.media.ImageWriter
-import android.util.Pair
-import android.util.Size
-import android.view.Surface
-import androidx.camera.core.impl.CameraCaptureCallback
-import androidx.camera.core.impl.CaptureBundle
-import androidx.camera.core.impl.CaptureProcessor
-import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.ImageReaderProxy
-import androidx.camera.core.impl.TagBundle
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.testing.fakes.FakeCameraCaptureResult
-import androidx.camera.testing.fakes.FakeCaptureStage
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.async
-import kotlinx.coroutines.runBlocking
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
-
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = 23) // This test uses ImageWriter which is supported from api 23.
-class ProcessingImageReaderDeviceTest {
- private companion object Bundle {
- private const val CAPTURE_ID_0 = 0
- private const val CAPTURE_ID_1 = 1
- private const val TIMESTAMP_0 = 0L
- private const val TIMESTAMP_1 = 1000L
- }
-
- // A processor that will generate a garbage image but has the timestamp of the first image in
- // the bundle
- private val mProcessor = object : CaptureProcessor {
- private lateinit var mImageWriter: ImageWriter
-
- override fun onOutputSurface(surface: Surface, imageFormat: Int) {
- mImageWriter = ImageWriter.newInstance(surface, 2)
- }
-
- override fun process(bundle: ImageProxyBundle) {
- val image = mImageWriter.dequeueInputImage()
- image.timestamp = bundle.getImageProxy(bundle.captureIds[0]).get().imageInfo.timestamp
- mImageWriter.queueInputImage(image)
- }
-
- override fun onResolutionUpdate(size: Size) = Unit
- }
-
- private val mCaptureStage0 = FakeCaptureStage(CAPTURE_ID_0, null)
- private val mCaptureStage1 = FakeCaptureStage(CAPTURE_ID_1, null)
-
- private lateinit var mCaptureBundle: CaptureBundle
-
- @Before
- fun setUp() {
- mCaptureBundle = CaptureBundles.createCaptureBundle(mCaptureStage0, mCaptureStage1)
- }
-
- @Test
- fun processesImage_whenImageInBundleEnqueued() = runBlocking {
- val processingImageReader = ProcessingImageReader.Builder(
- 640,
- 480,
- ImageFormat.YUV_420_888,
- 2,
- mCaptureBundle,
- mProcessor
- ).build()
-
- val job = async {
- suspendCoroutine<ImageProxy?> { cont ->
- // Waiting on the ProcessingImageReader to produce an ImageProxy
- processingImageReader.setOnImageAvailableListener(
- ImageReaderProxy.OnImageAvailableListener { imageReader ->
- cont.resume(imageReader.acquireNextImage())
- },
- CameraXExecutors.directExecutor()
- )
-
- processingImageReader.setCaptureBundle(mCaptureBundle)
- val imageWriter = ImageWriter.newInstance(processingImageReader.surface!!, 2)
- val callback = processingImageReader.cameraCaptureCallback!!
-
- // Trigger the bundle of images required for processing to occur
- triggerImage(imageWriter, callback, TIMESTAMP_0, CAPTURE_ID_0)
- triggerImage(imageWriter, callback, TIMESTAMP_1, CAPTURE_ID_1)
- }
- }
- val image = job.await()
-
- // Check the values of the images that are captured
- assertThat(image).isNotNull()
- assertThat(image!!.imageInfo.timestamp).isEqualTo(TIMESTAMP_0)
- }
-
- private fun triggerImage(
- imageWriter: ImageWriter,
- callback: CameraCaptureCallback,
- timestamp: Long,
- captureId: Int
- ) {
- val image = imageWriter.dequeueInputImage()
- image.timestamp = timestamp
- imageWriter.queueInputImage(image)
- val fakeCameraCaptureResult = FakeCameraCaptureResult()
- fakeCameraCaptureResult.timestamp = timestamp
- val tagBundle = TagBundle.create(
- Pair(
- mCaptureBundle.hashCode().toString(),
- captureId
- )
- )
- fakeCameraCaptureResult.setTag(tagBundle)
- callback.onCaptureCompleted(fakeCameraCaptureResult)
- }
-}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
index d5b344c..ebe18d2 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
@@ -351,7 +351,7 @@
autoCleanup: Boolean = true,
onInvalidated: () -> Unit = {},
): SurfaceRequest {
- val request = SurfaceRequest(size, FakeCamera(), false, expectedFrameRate, onInvalidated)
+ val request = SurfaceRequest(size, FakeCamera(), expectedFrameRate, onInvalidated)
if (autoCleanup) {
surfaceRequests.add(request)
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
index 884de88..a16b6f3 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
@@ -303,7 +303,7 @@
}
private fun createInputSurfaceRequest(): SurfaceRequest {
- return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera, false) {}.apply {
+ return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera) {}.apply {
inputSurfaceRequestsToClose.add(this)
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
index 8fad00e..bf3864f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraEffect.java
@@ -17,8 +17,6 @@
import static androidx.core.util.Preconditions.checkArgument;
-import android.os.Build;
-
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,21 +30,49 @@
/**
* An effect for one or multiple camera outputs.
*
- * <p>A {@link CameraEffect} class contains 2 types of information, the processor and the
- * configuration.
+ * <p>This API allows the implementer to inject code into CameraX pipeline and apply visual
+ * effects, such as a portrait effect. The {@link CameraEffect} class contains two types of
+ * information, the processor and the configuration.
* <ul>
- * <li> The processor is an implementation of either {@link SurfaceProcessor} or
+ * <li>The processor is an implementation of either {@link SurfaceProcessor} or
* {@link ImageProcessor}. It consumes original camera frames from CameraX, applies the effect,
* and returns the processed frames back to CameraX.
- * <li> The configuration provides information on how the processor should be injected into the
- * pipeline. For example, the target {@link UseCase}s where the effect should be applied.
+ * <li>The configuration provides information on how the processor should be injected into the
+ * pipeline. For example, the target {@link UseCase}s where the effect should be applied. It may
+ * also contain information about camera configuration. For example, the exposure level.
* </ul>
*
- * @hide
+ * <p>If CameraX fails to send frames to the {@link CameraEffect}, the error will be
+ * delivered to the app via error callbacks such as
+ * {@link ImageCapture.OnImageCapturedCallback#onError}. If {@link CameraEffect} fails to
+ * process and return the frames, for example, unable to allocate the resources for image
+ * processing, it must throw {@link Exception} in the processor implementation. The
+ * {@link Exception} will be caught and forwarded to the app via error callbacks. Please see the
+ * Javadoc of the processor interfaces for details.
+ *
+ * <p>Extend this class to create specific effects. The {@link Executor} provided in the
+ * constructors will be used by CameraX to call the processors.
+ *
+ * <p>Code sample for a portrait effect that targets the {@link Preview} {@link UseCase}:
+ *
+ * <pre><code>
+ * class PortraitPreviewEffect extends CameraEffect {
+ * PortraitPreviewEffect() {
+ * super(PREVIEW, getExecutor(), getSurfaceProcessor());
+ * }
+ *
+ * private static Executor getExecutor() {
+ * // Returns an executor for calling the SurfaceProcessor
+ * }
+ *
+ * private static SurfaceProcessor getSurfaceProcessor() {
+ * // Return a SurfaceProcessor implementation that applies a portrait effect.
+ * }
+ * }
+ * </code></pre>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class CameraEffect {
+@RequiresApi(21)
+public abstract class CameraEffect {
/**
* Bitmask options for the effect targets.
@@ -54,8 +80,8 @@
* @hide
*/
@Retention(RetentionPolicy.SOURCE)
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- @IntDef(flag = true, value = {PREVIEW, VIDEO_CAPTURE, IMAGE_CAPTURE})
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @IntDef(flag = true, value = {PREVIEW, IMAGE_CAPTURE})
public @interface Targets {
}
@@ -66,7 +92,10 @@
/**
* Bitmask option to indicate that CameraX should apply this effect to {@code VideoCapture}.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static final int VIDEO_CAPTURE = 1 << 1;
/**
@@ -93,7 +122,9 @@
* @param imageProcessor a {@link ImageProcessor} implementation. Once the effect is active,
* CameraX will send frames to the {@link ImageProcessor} on the
* {@param executor}, and deliver the processed frames to the app.
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
protected CameraEffect(
@Targets int targets,
@NonNull Executor executor,
@@ -141,7 +172,7 @@
/**
* Gets the {@link Executor} associated with this effect.
*
- * <p>This method returns the value set in {@link CameraEffect}'s constructor.
+ * <p>This method returns the value set in the constructor.
*/
@NonNull
public Executor getExecutor() {
@@ -158,8 +189,11 @@
/**
* Gets the {@link ImageProcessor} associated with this effect.
+ *
+ * @hide
*/
@Nullable
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public ImageProcessor getImageProcessor() {
return mImageProcessor;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 17254cc..45076b8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -17,7 +17,6 @@
package androidx.camera.core;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_BUFFER_FORMAT;
-import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_BUNDLE;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_DEFAULT_SESSION_CONFIG;
@@ -92,10 +91,7 @@
import androidx.camera.core.impl.CameraConfig;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.CameraInternal;
-import androidx.camera.core.impl.CaptureBundle;
import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
import androidx.camera.core.impl.Config;
import androidx.camera.core.impl.ConfigProvider;
import androidx.camera.core.impl.DeferrableSurface;
@@ -117,7 +113,6 @@
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.core.internal.IoConfig;
import androidx.camera.core.internal.TargetConfig;
-import androidx.camera.core.internal.YuvToJpegProcessor;
import androidx.camera.core.internal.compat.quirk.SoftwareJpegEncodingPreferredQuirk;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
import androidx.camera.core.internal.utils.ImageUtil;
@@ -136,6 +131,7 @@
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
@@ -143,12 +139,8 @@
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -318,9 +310,6 @@
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached constant] - Is only valid when the UseCase is attached to a camera.
////////////////////////////////////////////////////////////////////////////////////////////
-
- private ExecutorService mExecutor;
-
private CaptureConfig mCaptureConfig;
/**
@@ -339,11 +328,6 @@
@SuppressWarnings("WeakerAccess")
SafeCloseImageReaderProxy mImageReader;
- @SuppressWarnings("WeakerAccess")
- ProcessingImageReader mProcessingImageReader;
-
- private ListenableFuture<Void> mImageReaderCloseFuture = Futures.immediateFuture(null);
-
/** Callback used to match the {@link ImageProxy} with the {@link ImageInfo}. */
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
@@ -391,7 +375,6 @@
return createPipelineWithNode(cameraId, config, resolution);
}
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
- YuvToJpegProcessor softwareJpegProcessor = null;
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
getCameraControl().addZslConfig(sessionConfigBuilder);
@@ -419,32 +402,6 @@
throw new IllegalArgumentException("Unsupported image format:" + getImageFormat());
}
mImageReader = new SafeCloseImageReaderProxy(imageReader);
- } else if (mUseSoftwareJpeg) {
- CaptureProcessor captureProcessor;
- int inputFormat = getImageFormat();
- // API check to satisfy linter
- if (Build.VERSION.SDK_INT >= 26) {
- Logger.i(TAG, "Using software JPEG encoder.");
- captureProcessor = softwareJpegProcessor =
- new YuvToJpegProcessor(getJpegQualityInternal(), MAX_IMAGES);
- } else {
- // Note: This should never be hit due to SDK_INT check before setting
- // useSoftwareJpeg.
- throw new IllegalStateException("Software JPEG only supported on API 26+");
- }
-
- // TODO: To allow user to use an Executor for the image processing.
- mProcessingImageReader = new ProcessingImageReader.Builder(
- resolution.getWidth(),
- resolution.getHeight(),
- inputFormat,
- MAX_IMAGES,
- CaptureBundles.singleDefaultCaptureBundle(),
- captureProcessor
- ).setPostProcessExecutor(mExecutor).setOutputFormat(ImageFormat.JPEG).build();
-
- mMetadataMatchingCaptureCallback = mProcessingImageReader.getCameraCaptureCallback();
- mImageReader = new SafeCloseImageReaderProxy(mProcessingImageReader);
} else {
MetadataImageReader metadataImageReader = new MetadataImageReader(resolution.getWidth(),
resolution.getHeight(), getImageFormat(), MAX_IMAGES);
@@ -457,27 +414,8 @@
new CancellationException("Request is canceled."));
}
- final YuvToJpegProcessor finalSoftwareJpegProcessor = softwareJpegProcessor;
-
mImageCaptureRequestProcessor = new ImageCaptureRequestProcessor(MAX_IMAGES,
- this::takePictureInternal, finalSoftwareJpegProcessor == null ? null :
- (ImageCaptureRequestProcessor.RequestProcessCallback) imageCaptureRequest -> {
- //noinspection ConstantConditions
- if (Build.VERSION.SDK_INT >= 26) {
- // Updates output JPEG compression quality of YuvToJpegProcessor
- // according to current request. This was determined by whether the
- // final output image needs to be cropped (uncompress and recompress)
- // again when the capture request was created.
- finalSoftwareJpegProcessor.setJpegQuality(
- imageCaptureRequest.mJpegQuality);
-
- // Updates output rotation degrees value to the YuvToJpegProcessor so
- // that it can write the correct value to the ExifData in the output
- // JPEG image file.
- finalSoftwareJpegProcessor.setRotationDegrees(
- imageCaptureRequest.mRotationDegrees);
- }
- });
+ this::takePictureInternal);
// By default close images that come from the listener.
mImageReader.setOnImageAvailableListener(mClosingListener,
@@ -494,9 +432,6 @@
/* get the surface image format using getImageFormat */
getImageFormat());
- mImageReaderCloseFuture =
- mProcessingImageReader != null ? mProcessingImageReader.getCloseFuture()
- : Futures.immediateFuture(null);
mDeferrableSurface.getTerminationFuture().addListener(mImageReader::safeClose,
CameraXExecutors.mainThreadExecutor());
@@ -559,8 +494,6 @@
DeferrableSurface deferrableSurface = mDeferrableSurface;
mDeferrableSurface = null;
mImageReader = null;
- mProcessingImageReader = null;
- mImageReaderCloseFuture = Futures.immediateFuture(null);
if (deferrableSurface != null) {
deferrableSurface.close();
@@ -611,9 +544,8 @@
@Override
protected UseCaseConfig<?> onMergeConfig(@NonNull CameraInfoInternal cameraInfo,
@NonNull UseCaseConfig.Builder<?, ?, ?> builder) {
- if (cameraInfo.getCameraQuirks().contains(
- SoftwareJpegEncodingPreferredQuirk.class)) {
- // Request software JPEG encoder if quirk exists on this device and the software JPEG
+ if (cameraInfo.getCameraQuirks().contains(SoftwareJpegEncodingPreferredQuirk.class)) {
+ // Request software JPEG encoder if quirk exists on this device, and the software JPEG
// option has not already been explicitly set.
if (Boolean.FALSE.equals(builder.getMutableConfig().retrieveOption(
OPTION_USE_SOFTWARE_JPEG_ENCODER, true))) {
@@ -625,7 +557,7 @@
}
}
- // If software JPEG is requested, disable if it can't be supported on current API level.
+ // If software JPEG is requested, disable if it is incompatible.
boolean useSoftwareJpeg = enforceSoftwareJpegConstraints(builder.getMutableConfig());
// Update the input format base on the other options set (mainly whether processing
@@ -1293,11 +1225,10 @@
completer.setException(throwable);
}
},
- mExecutor);
+ CameraXExecutors.mainThreadExecutor());
completer.addCancellationListener(() -> future.cancel(true),
CameraXExecutors.directExecutor());
-
return "takePictureInternal";
});
}
@@ -1328,24 +1259,13 @@
@GuardedBy("mLock")
private final ImageCaptor mImageCaptor;
-
private final int mMaxImages;
-
- @Nullable
- private final RequestProcessCallback mRequestProcessCallback;
-
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final Object mLock = new Object();
ImageCaptureRequestProcessor(int maxImages, @NonNull ImageCaptor imageCaptor) {
- this(maxImages, imageCaptor, null);
- }
-
- ImageCaptureRequestProcessor(int maxImages, @NonNull ImageCaptor imageCaptor,
- @Nullable RequestProcessCallback requestProcessCallback) {
mMaxImages = maxImages;
mImageCaptor = imageCaptor;
- mRequestProcessCallback = requestProcessCallback;
}
/**
@@ -1448,9 +1368,6 @@
}
mCurrentRequest = imageCaptureRequest;
- if (mRequestProcessCallback != null) {
- mRequestProcessCallback.onPreProcessRequest(mCurrentRequest);
- }
mCurrentRequestFuture = mImageCaptor.capture(imageCaptureRequest);
Futures.addCallback(mCurrentRequestFuture, new FutureCallback<ImageProxy>() {
@Override
@@ -1539,17 +1456,16 @@
*
* @return {@code true} if software JPEG will be used after applying constraints.
*/
- static boolean enforceSoftwareJpegConstraints(@NonNull MutableConfig mutableConfig) {
+ boolean enforceSoftwareJpegConstraints(@NonNull MutableConfig mutableConfig) {
// Software encoder currently only supports API 26+.
if (Boolean.TRUE.equals(
mutableConfig.retrieveOption(OPTION_USE_SOFTWARE_JPEG_ENCODER, false))) {
boolean supported = true;
- if (Build.VERSION.SDK_INT < 26) {
- Logger.w(TAG, "Software JPEG only supported on API 26+, but current API level is "
- + Build.VERSION.SDK_INT);
+ if (isSessionProcessorEnabledInCurrentCamera()) {
+ // SessionProcessor requires JPEG input format so it is incompatible with SW Jpeg.
+ Logger.w(TAG, "Software JPEG cannot be used with Extensions.");
supported = false;
}
-
Integer bufferFormat = mutableConfig.retrieveOption(OPTION_BUFFER_FORMAT, null);
if (bufferFormat != null && bufferFormat != ImageFormat.JPEG) {
Logger.w(TAG, "Software JPEG cannot be used with non-JPEG output buffer format.");
@@ -1574,20 +1490,9 @@
@RestrictTo(Scope.LIBRARY_GROUP)
@Override
public void onDetached() {
- ListenableFuture<Void> imageReaderCloseFuture = mImageReaderCloseFuture;
-
abortImageCaptureRequests();
clearPipeline();
mUseSoftwareJpeg = false;
-
- // Shutdowns the executor after mImageReader is closed. This can avoid
- // RejectedExecutionException if a ProcessingImageReader is used to processing the
- // captured images.
- ExecutorService executorService = mExecutor;
- if (executorService != null) {
- imageReaderCloseFuture.addListener(executorService::shutdown,
- CameraXExecutors.directExecutor());
- }
}
/**
@@ -1609,21 +1514,6 @@
CameraInternal camera = getCamera();
checkNotNull(camera, "Attached camera cannot be null");
-
- mExecutor =
- Executors.newFixedThreadPool(
- 1,
- new ThreadFactory() {
- private final AtomicInteger mId = new AtomicInteger(0);
-
- @Override
- public Thread newThread(@NonNull Runnable r) {
- return new Thread(
- r,
- CameraXThreads.TAG + "image_capture_"
- + mId.getAndIncrement());
- }
- });
}
/**
@@ -1658,80 +1548,33 @@
ListenableFuture<Void> issueTakePicture(@NonNull ImageCaptureRequest imageCaptureRequest) {
Logger.d(TAG, "issueTakePicture");
- final List<CaptureConfig> captureConfigs = new ArrayList<>();
- String tagBundleKey = null;
+ final CaptureConfig.Builder builder = new CaptureConfig.Builder();
+ builder.setTemplateType(mCaptureConfig.getTemplateType());
- CaptureBundle captureBundle;
- if (mProcessingImageReader != null) {
- // If the Processor is provided, check if we have valid CaptureBundle and update
- // ProcessingImageReader before actually issuing a take picture request.
- captureBundle = CaptureBundles.singleDefaultCaptureBundle();
- mProcessingImageReader.setCaptureBundle(captureBundle);
- mProcessingImageReader.setOnProcessingErrorCallback(
- CameraXExecutors.directExecutor(),
- (message, cause) -> {
- Logger.e(TAG, "Processing image failed! " + message);
- imageCaptureRequest.notifyCallbackError(ERROR_CAPTURE_FAILED, message,
- cause);
- });
- tagBundleKey = mProcessingImageReader.getTagBundleKey();
- } else {
- captureBundle = CaptureBundles.singleDefaultCaptureBundle();
- if (captureBundle == null) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture cannot set empty CaptureBundle."));
- }
+ // Add the default implementation options of ImageCapture
+ builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
+ builder.addAllCameraCaptureCallbacks(
+ mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
- List<CaptureStage> captureStages = captureBundle.getCaptureStages();
- if (captureStages == null) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture has CaptureBundle with null capture stages"));
- }
+ builder.addSurface(mDeferrableSurface);
- if (captureStages.size() > 1) {
- return Futures.immediateFailedFuture(new IllegalArgumentException(
- "ImageCapture have no CaptureProcess set with CaptureBundle size > 1."));
+ // Only sets the JPEG rotation and quality capture request options when capturing
+ // images in JPEG format. Some devices do not handle these CaptureRequest key values
+ // when capturing a non-JPEG image. Setting these capture requests and checking the
+ // returned capture results for specific purpose might cause problems. See b/204375890.
+ if (getImageFormat() == ImageFormat.JPEG) {
+ // Add the dynamic implementation options of ImageCapture
+ if (EXIF_ROTATION_AVAILABILITY.isRotationOptionSupported()) {
+ builder.addImplementationOption(CaptureConfig.OPTION_ROTATION,
+ imageCaptureRequest.mRotationDegrees);
}
+ builder.addImplementationOption(CaptureConfig.OPTION_JPEG_QUALITY,
+ imageCaptureRequest.mJpegQuality);
}
- for (final CaptureStage captureStage : captureBundle.getCaptureStages()) {
- final CaptureConfig.Builder builder = new CaptureConfig.Builder();
- builder.setTemplateType(mCaptureConfig.getTemplateType());
+ builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
- // Add the default implementation options of ImageCapture
- builder.addImplementationOptions(mCaptureConfig.getImplementationOptions());
- builder.addAllCameraCaptureCallbacks(
- mSessionConfigBuilder.getSingleCameraCaptureCallbacks());
-
- builder.addSurface(mDeferrableSurface);
-
- // Only sets the JPEG rotation and quality capture request options when capturing
- // images in JPEG format. Some devices do not handle these CaptureRequest key values
- // when capturing a non-JPEG image. Setting these capture requests and checking the
- // returned capture results for specific purpose might cause problems. See b/204375890.
- if (getImageFormat() == ImageFormat.JPEG) {
- // Add the dynamic implementation options of ImageCapture
- if (EXIF_ROTATION_AVAILABILITY.isRotationOptionSupported()) {
- builder.addImplementationOption(CaptureConfig.OPTION_ROTATION,
- imageCaptureRequest.mRotationDegrees);
- }
- builder.addImplementationOption(CaptureConfig.OPTION_JPEG_QUALITY,
- imageCaptureRequest.mJpegQuality);
- }
-
- // Add the implementation options required by the CaptureStage
- builder.addImplementationOptions(
- captureStage.getCaptureConfig().getImplementationOptions());
-
- // Use CaptureBundle object as the key for TagBundle
- if (tagBundleKey != null) {
- builder.addTag(tagBundleKey, captureStage.getId());
- }
- builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
- captureConfigs.add(builder.build());
- }
-
- return submitStillCaptureRequest(captureConfigs);
+ return submitStillCaptureRequest(Arrays.asList(builder.build()));
}
/**
@@ -1790,33 +1633,17 @@
return false;
}
if (isSessionProcessorEnabledInCurrentCamera()) {
- // Use old pipeline for advanced Extensions.
+ // Use old pipeline when extension is enabled.
return false;
}
- if (getCaptureStageSize(config) > 1) {
- // Use old pipeline for multiple stages capture.
- return false;
- }
- if (requireNonNull(config.retrieveOption(OPTION_INPUT_FORMAT, ImageFormat.JPEG))
- != ImageFormat.JPEG) {
+
+ if (config.getBufferFormat(ImageFormat.JPEG) != ImageFormat.JPEG) {
// Use old pipeline for non-JPEG output format.
return false;
}
return mUseProcessingPipeline;
}
- private int getCaptureStageSize(@NonNull ImageCaptureConfig config) {
- CaptureBundle captureBundle = config.getCaptureBundle(null);
- if (captureBundle == null) {
- return 1;
- }
- List<CaptureStage> captureStages = captureBundle.getCaptureStages();
- if (captureStages == null) {
- return 1;
- }
- return captureStages.size();
- }
-
/**
* Creates the pipeline for both capture request configuration and image post-processing.
*
@@ -2770,20 +2597,6 @@
}
/**
- * Sets the {@link CaptureBundle}.
- *
- * @param captureBundle The requested capture bundle for extension.
- * @return The current Builder.
- * @hide
- */
- @RestrictTo(Scope.LIBRARY_GROUP)
- @NonNull
- public Builder setCaptureBundle(@NonNull CaptureBundle captureBundle) {
- getMutableConfig().insertOption(OPTION_CAPTURE_BUNDLE, captureBundle);
- return this;
- }
-
- /**
* Sets the {@link ImageFormat} of the {@link ImageProxy} returned by the
* {@link ImageCapture.OnImageCapturedCallback}.
*
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..6a89e2b6 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;
@@ -219,7 +219,7 @@
// CaptureProcessor. e.g. only update the output Surface (between Processor/App), and
// still use the same input Surface (between Camera/Processor). It's just simpler for now.
final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
- /* isRGBA8888Required */ false, this::notifyReset);
+ this::notifyReset);
mCurrentSurfaceRequest = surfaceRequest;
if (mSurfaceProvider != null) {
@@ -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/ProcessingException.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
new file mode 100644
index 0000000..139162e
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingException.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+/**
+ * Exception throw when effects post-processing fails.
+ *
+ * <p>Implementation of {@link SurfaceProcessor} throws this exception from
+ * {@link SurfaceProcessor#onInputSurface} or {@link SurfaceProcessor#onOutputSurface} when an
+ * error occurs during effect processing.
+ *
+ * @see SurfaceProcessor
+ */
+public class ProcessingException extends Exception {
+}
+
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java b/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
deleted file mode 100644
index 97fb3ec..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/ProcessingImageReader.java
+++ /dev/null
@@ -1,598 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.graphics.ImageFormat;
-import android.media.ImageReader;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CaptureBundle;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
-/**
- * An {@link ImageReaderProxy} which takes one or more {@link android.media.Image}, processes it,
- * then output the final result {@link ImageProxy} to
- * {@link ImageReaderProxy.OnImageAvailableListener}.
- *
- * <p>ProcessingImageReader takes {@link CaptureBundle} as the expected set of
- * {@link CaptureStage}. Once all the ImageProxy from the captures are ready. It invokes
- * the {@link CaptureProcessor} set, then returns a single output ImageProxy to
- * OnImageAvailableListener.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-class ProcessingImageReader implements ImageReaderProxy {
- private static final String TAG = "ProcessingImageReader";
-
- // Exif metadata are restricted in size to 64 kB in JPEG images because according to
- // the specification this information must be contained within a single JPEG APP1
- // segment. (See: https://en.wikipedia.org/wiki/Exif)
- private static final int EXIF_MAX_SIZE_BYTES = 64000;
-
- final Object mLock = new Object();
-
- // Callback when Image is ready from InputImageReader.
- private ImageReaderProxy.OnImageAvailableListener mTransformedListener =
- new ImageReaderProxy.OnImageAvailableListener() {
- @Override
- public void onImageAvailable(@NonNull ImageReaderProxy reader) {
- imageIncoming(reader);
- }
- };
-
- // Callback when Image is ready from OutputImageReader.
- private ImageReaderProxy.OnImageAvailableListener mImageProcessedListener =
- new ImageReaderProxy.OnImageAvailableListener() {
- @Override
- public void onImageAvailable(@NonNull ImageReaderProxy reader) {
- // Callback the output OnImageAvailableListener.
- ImageReaderProxy.OnImageAvailableListener listener;
- Executor executor;
- synchronized (mLock) {
- listener = mListener;
- executor = mExecutor;
-
- // Resets SettableImageProxyBundle after the processor finishes processing.
- mSettableImageProxyBundle.reset();
- setupSettableImageProxyBundleCallbacks();
- }
- if (listener != null) {
- if (executor != null) {
- executor.execute(
- () -> listener.onImageAvailable(ProcessingImageReader.this));
- } else {
- listener.onImageAvailable(ProcessingImageReader.this);
- }
- }
- }
- };
-
- // Callback when all the ImageProxies in SettableImageProxyBundle are ready.
- private FutureCallback<List<ImageProxy>> mCaptureStageReadyCallback =
- new FutureCallback<List<ImageProxy>>() {
- @Override
- public void onSuccess(@Nullable List<ImageProxy> imageProxyList) {
- SettableImageProxyBundle settableImageProxyBundle;
- OnProcessingErrorCallback errorCallback;
- Executor errorCallbackExecutor;
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
- mProcessing = true;
- settableImageProxyBundle = mSettableImageProxyBundle;
- errorCallback = mOnProcessingErrorCallback;
- errorCallbackExecutor = mErrorCallbackExecutor;
- }
- try {
- mCaptureProcessor.process(settableImageProxyBundle);
- } catch (Exception e) {
- synchronized (mLock) {
- // Resets mSettableImageProxyBundle to close the held images.
- mSettableImageProxyBundle.reset();
-
- if (errorCallback != null && errorCallbackExecutor != null) {
- errorCallbackExecutor.execute(
- () -> errorCallback.notifyProcessingError(
- e.getMessage(), e.getCause()));
- }
- }
- }
-
- synchronized (mLock) {
- mProcessing = false;
- }
-
- closeAndCompleteFutureIfNecessary();
- }
-
- @Override
- public void onFailure(@NonNull Throwable throwable) {
-
- }
- };
-
- @GuardedBy("mLock")
- boolean mClosed = false;
-
- @GuardedBy("mLock")
- boolean mProcessing = false;
-
- @GuardedBy("mLock")
- final ImageReaderProxy mInputImageReader;
-
- @GuardedBy("mLock")
- final ImageReaderProxy mOutputImageReader;
-
- @GuardedBy("mLock")
- @Nullable
- ImageReaderProxy.OnImageAvailableListener mListener;
-
- @GuardedBy("mLock")
- @Nullable
- Executor mExecutor;
-
- @GuardedBy("mLock")
- CallbackToFutureAdapter.Completer<Void> mCloseCompleter;
- @GuardedBy("mLock")
- private ListenableFuture<Void> mCloseFuture;
-
- /** The Executor to execute the image post processing task. */
- @NonNull
- final Executor mPostProcessExecutor;
-
- @NonNull
- final CaptureProcessor mCaptureProcessor;
-
- @NonNull
- private final ListenableFuture<Void> mUnderlyingCaptureProcessorCloseFuture;
-
- private String mTagBundleKey = new String();
-
- @GuardedBy("mLock")
- @NonNull
- SettableImageProxyBundle mSettableImageProxyBundle =
- new SettableImageProxyBundle(Collections.emptyList(), mTagBundleKey);
-
- private final List<Integer> mCaptureIdList = new ArrayList<>();
-
- private ListenableFuture<List<ImageProxy>> mSettableImageProxyFutureList =
- Futures.immediateFuture(new ArrayList<>());
-
- @GuardedBy("mLock")
- OnProcessingErrorCallback mOnProcessingErrorCallback;
-
- @GuardedBy("mLock")
- Executor mErrorCallbackExecutor;
-
- ProcessingImageReader(@NonNull Builder builder) {
- if (builder.mInputImageReader.getMaxImages()
- < builder.mCaptureBundle.getCaptureStages().size()) {
- throw new IllegalArgumentException(
- "MetadataImageReader is smaller than CaptureBundle.");
- }
-
- mInputImageReader = builder.mInputImageReader;
-
- // For JPEG ImageReaders, the Surface that is created will have format BLOB which can
- // only be allocated with a height of 1. The output Image from the image reader will read
- // its dimensions from the JPEG data's EXIF in order to set the final dimensions.
- int outputWidth = mInputImageReader.getWidth();
- int outputHeight = mInputImageReader.getHeight();
-
- if (builder.mOutputFormat == ImageFormat.JPEG) {
- // The output JPEG compression quality is 100 when taking a picture in MAX_QUALITY
- // mode. It might cause the compressed data size exceeds image's width * height.
- // YUV_420_888 should be 1.5 times of image's width * height. The compressed data
- // size shouldn't exceed it. Therefore, scales the output image reader byte buffer to
- // 1.5 times when the JPEG compression quality setting is 100.
- outputWidth = (int) (outputWidth * outputHeight * 1.5f) + EXIF_MAX_SIZE_BYTES;
- outputHeight = 1;
- }
- mOutputImageReader = new AndroidImageReaderProxy(
- ImageReader.newInstance(outputWidth, outputHeight, builder.mOutputFormat,
- mInputImageReader.getMaxImages()));
-
- mPostProcessExecutor = builder.mPostProcessExecutor;
- mCaptureProcessor = builder.mCaptureProcessor;
- mCaptureProcessor.onOutputSurface(mOutputImageReader.getSurface(), builder.mOutputFormat);
- mCaptureProcessor.onResolutionUpdate(
- new Size(mInputImageReader.getWidth(), mInputImageReader.getHeight()));
-
- mUnderlyingCaptureProcessorCloseFuture = mCaptureProcessor.getCloseFuture();
-
- setCaptureBundle(builder.mCaptureBundle);
- }
-
- @Override
- @Nullable
- public ImageProxy acquireLatestImage() {
- synchronized (mLock) {
- return mOutputImageReader.acquireLatestImage();
- }
- }
-
- @Override
- @Nullable
- public ImageProxy acquireNextImage() {
- synchronized (mLock) {
- return mOutputImageReader.acquireNextImage();
- }
- }
-
- @Override
- public void close() {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- // Prevent the ImageAvailableListener from being triggered after the close function
- // is called.
- mInputImageReader.clearOnImageAvailableListener();
- mOutputImageReader.clearOnImageAvailableListener();
-
- mClosed = true;
- }
-
- mCaptureProcessor.close();
- closeAndCompleteFutureIfNecessary();
- }
-
- void closeAndCompleteFutureIfNecessary() {
- boolean closed;
- boolean processing;
- CallbackToFutureAdapter.Completer<Void> closeCompleter;
-
- synchronized (mLock) {
- closed = mClosed;
- processing = mProcessing;
- closeCompleter = mCloseCompleter;
-
- // If the CaptureProcessor is in the middle of processing then don't close the
- // ImageReaderProxys and associated ImageProxy. Let the processing complete before
- // closing them.
- if (closed && !processing) {
- mInputImageReader.close();
- mSettableImageProxyBundle.close();
- mOutputImageReader.close();
- }
- }
-
- if (closed && !processing) {
- // Complete the capture process pipeline's close future after the underlying capture
- // processor is closed.
- mUnderlyingCaptureProcessorCloseFuture.addListener(() -> {
- cancelSettableImageProxyBundleFutureList();
- if (closeCompleter != null) {
- closeCompleter.set(null);
- }
- }, CameraXExecutors.directExecutor());
- }
- }
-
- /**
- * Returns a future that will complete when the ProcessingImageReader is actually closed.
- *
- * @return A future that signals when the ProcessingImageReader is actually closed
- * (after all processing). Cancelling this future has no effect.
- */
- @NonNull
- ListenableFuture<Void> getCloseFuture() {
- ListenableFuture<Void> closeFuture;
- synchronized (mLock) {
- if (mClosed && !mProcessing) {
- // Everything should be closed but still need to wait for underlying capture
- // processors being closed.
- closeFuture = Futures.transform(mUnderlyingCaptureProcessorCloseFuture,
- nullVoid -> null, CameraXExecutors.directExecutor());
- } else {
- if (mCloseFuture == null) {
- mCloseFuture = CallbackToFutureAdapter.getFuture(completer -> {
- // Should already be locked, but lock again to satisfy linter.
- synchronized (mLock) {
- mCloseCompleter = completer;
- }
- return "ProcessingImageReader-close";
- });
- }
- closeFuture = Futures.nonCancellationPropagating(mCloseFuture);
- }
- }
- return closeFuture;
- }
-
- @Override
- public int getHeight() {
- synchronized (mLock) {
- return mInputImageReader.getHeight();
- }
- }
-
- @Override
- public int getWidth() {
- synchronized (mLock) {
- return mInputImageReader.getWidth();
- }
- }
-
- @Override
- public int getImageFormat() {
- synchronized (mLock) {
- return mOutputImageReader.getImageFormat();
- }
- }
-
- @Override
- public int getMaxImages() {
- synchronized (mLock) {
- return mInputImageReader.getMaxImages();
- }
- }
-
- @Nullable
- @Override
- public Surface getSurface() {
- synchronized (mLock) {
- return mInputImageReader.getSurface();
- }
- }
-
- @Override
- public void setOnImageAvailableListener(@NonNull OnImageAvailableListener listener,
- @NonNull Executor executor) {
- synchronized (mLock) {
- mListener = Preconditions.checkNotNull(listener);
- mExecutor = Preconditions.checkNotNull(executor);
- mInputImageReader.setOnImageAvailableListener(mTransformedListener, executor);
- mOutputImageReader.setOnImageAvailableListener(mImageProcessedListener, executor);
- }
- }
-
- @Override
- public void clearOnImageAvailableListener() {
- synchronized (mLock) {
- mListener = null;
- mExecutor = null;
- mInputImageReader.clearOnImageAvailableListener();
- mOutputImageReader.clearOnImageAvailableListener();
-
- if (!mProcessing) {
- mSettableImageProxyBundle.close();
- }
- }
- }
-
- /** Sets a CaptureBundle */
- public void setCaptureBundle(@NonNull CaptureBundle captureBundle) {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- cancelSettableImageProxyBundleFutureList();
-
- if (captureBundle.getCaptureStages() != null) {
- if (mInputImageReader.getMaxImages() < captureBundle.getCaptureStages().size()) {
- throw new IllegalArgumentException(
- "CaptureBundle is larger than InputImageReader.");
- }
-
- mCaptureIdList.clear();
-
- for (CaptureStage captureStage : captureBundle.getCaptureStages()) {
- if (captureStage != null) {
- mCaptureIdList.add(captureStage.getId());
- }
- }
- }
-
- // Use the mCaptureBundle as the key for TagBundle
- mTagBundleKey = Integer.toString(captureBundle.hashCode());
- mSettableImageProxyBundle = new SettableImageProxyBundle(mCaptureIdList, mTagBundleKey);
- setupSettableImageProxyBundleCallbacks();
- }
- }
-
- private void cancelSettableImageProxyBundleFutureList() {
- synchronized (mLock) {
- if (!mSettableImageProxyFutureList.isDone()) {
- mSettableImageProxyFutureList.cancel(true);
- }
-
- mSettableImageProxyBundle.reset();
- }
- }
-
- /** Returns a TagBundleKey which is used in this processing image reader.*/
- @NonNull
- public String getTagBundleKey() {
- return mTagBundleKey;
- }
-
- /** Returns necessary camera callbacks to retrieve metadata from camera result. */
- @Nullable
- CameraCaptureCallback getCameraCaptureCallback() {
- synchronized (mLock) {
- if (mInputImageReader instanceof MetadataImageReader) {
- return ((MetadataImageReader) mInputImageReader).getCameraCaptureCallback();
- } else {
- return new CameraCaptureCallback() {};
- }
- }
- }
-
- /**
- * Sets {@link OnProcessingErrorCallback} to receive error notifications.
- *
- * @param executor The executor in which the callback methods will be run.
- * @param callback Callback to be invoked if an error occurs when processing the images.
- */
- public void setOnProcessingErrorCallback(@NonNull Executor executor,
- @NonNull OnProcessingErrorCallback callback) {
- synchronized (mLock) {
- mErrorCallbackExecutor = executor;
- mOnProcessingErrorCallback = callback;
- }
- }
-
- @GuardedBy("mLock")
- void setupSettableImageProxyBundleCallbacks() {
- List<ListenableFuture<ImageProxy>> futureList = new ArrayList<>();
- for (Integer id : mCaptureIdList) {
- futureList.add(mSettableImageProxyBundle.getImageProxy(id));
- }
-
- mSettableImageProxyFutureList = Futures.allAsList(futureList);
-
- Futures.addCallback(Futures.allAsList(futureList), mCaptureStageReadyCallback,
- mPostProcessExecutor);
- }
-
- // Incoming Image from InputImageReader. Acquires it and add to SettableImageProxyBundle.
- void imageIncoming(ImageReaderProxy imageReader) {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- ImageProxy image = null;
- try {
- image = imageReader.acquireNextImage();
- } catch (IllegalStateException e) {
- Logger.e(TAG, "Failed to acquire latest image.", e);
- } finally {
- if (image != null) {
- // Currently use the same key which intends to get a captureStage id value.
- Integer tagValue =
- (Integer) image.getImageInfo().getTagBundle().getTag(mTagBundleKey);
-
- if (!mCaptureIdList.contains(tagValue)) {
- Logger.w(TAG, "ImageProxyBundle does not contain this id: " + tagValue);
- image.close();
- } else {
- mSettableImageProxyBundle.addImageProxy(image);
- }
- }
- }
- }
- }
-
- /**
- * The builder to create a {@link ProcessingImageReader} object.
- */
- static final class Builder {
- @NonNull
- protected final ImageReaderProxy mInputImageReader;
- @NonNull
- protected final CaptureBundle mCaptureBundle;
- @NonNull
- protected final CaptureProcessor mCaptureProcessor;
-
- protected int mOutputFormat;
-
- @NonNull
- protected Executor mPostProcessExecutor = Executors.newSingleThreadExecutor();
-
- /**
- * Create a {@link Builder} with specific configurations.
- *
- * @param imageReader The input image reader.
- * @param captureBundle The {@link CaptureBundle} includes the processing information
- * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
- * ready
- */
- Builder(@NonNull ImageReaderProxy imageReader, @NonNull CaptureBundle captureBundle,
- @NonNull CaptureProcessor captureProcessor) {
- mInputImageReader = imageReader;
- mCaptureBundle = captureBundle;
- mCaptureProcessor = captureProcessor;
- mOutputFormat = imageReader.getImageFormat();
- }
-
- /**
- * Create a {@link Builder} with specific configurations.
- *
- * @param width Width of the ImageReader
- * @param height Height of the ImageReader
- * @param inputFormat Input image format
- * @param maxImages Maximum Image number the ImageReader can hold. The capacity
- * should be greater than the captureBundle size in order to hold
- * all the Images needed with this processing.
- * @param captureBundle The {@link CaptureBundle} includes the processing information
- * @param captureProcessor The {@link CaptureProcessor} to be invoked when the Images are
- * ready
- */
- Builder(int width, int height, int inputFormat, int maxImages,
- @NonNull CaptureBundle captureBundle, @NonNull CaptureProcessor captureProcessor) {
- this(new MetadataImageReader(width, height, inputFormat, maxImages), captureBundle,
- captureProcessor);
- }
-
- /**
- * Sets an Executor to execute the post-process of the image result.
- */
- @NonNull
- Builder setPostProcessExecutor(@NonNull Executor postProcessExecutor) {
- mPostProcessExecutor = postProcessExecutor;
- return this;
- }
-
- /**
- * Sets the output image format.
- */
- @NonNull
- Builder setOutputFormat(int outputFormat) {
- mOutputFormat = outputFormat;
- return this;
- }
-
- /**
- * Builds an {@link ProcessingImageReader} from current configurations.
- */
- ProcessingImageReader build() {
- return new ProcessingImageReader(this);
- }
- }
-
- /**
- * Callback for notifying processing errors.
- */
- interface OnProcessingErrorCallback {
- void notifyProcessingError(@Nullable String message, @Nullable Throwable cause);
- }
-}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java
deleted file mode 100644
index 8bf8a73..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/SettableImageProxyBundle.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import android.util.SparseArray;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * A {@link ImageProxyBundle} with a predefined set of captured ids. The {@link ListenableFuture}
- * for the capture id becomes valid when the corresponding {@link ImageProxy} has been set.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class SettableImageProxyBundle implements ImageProxyBundle {
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- final Object mLock = new Object();
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @GuardedBy("mLock")
- final SparseArray<CallbackToFutureAdapter.Completer<ImageProxy>> mCompleters =
- new SparseArray<>();
- /** Map of id to {@link ImageProxy} Future. */
- @GuardedBy("mLock")
- private final SparseArray<ListenableFuture<ImageProxy>> mFutureResults = new SparseArray<>();
-
- @GuardedBy("mLock")
- private final List<ImageProxy> mOwnedImageProxies = new ArrayList<>();
-
- private final List<Integer> mCaptureIdList;
- private String mTagBundleKey = null;
-
- // Whether or not the bundle has been closed or not
- @GuardedBy("mLock")
- private boolean mClosed = false;
-
- /**
- * Create a {@link ImageProxyBundle} for captures with the given ids.
- *
- * @param captureIds The set of captureIds contained by the ImageProxyBundle
- * @param tagBundleKey `The key for checking desired image from TagBundle
- */
- SettableImageProxyBundle(List<Integer> captureIds, String tagBundleKey) {
- mCaptureIdList = captureIds;
- mTagBundleKey = tagBundleKey;
- setup();
- }
-
- @Override
- @NonNull
- public ListenableFuture<ImageProxy> getImageProxy(int captureId) {
- synchronized (mLock) {
- if (mClosed) {
- throw new IllegalStateException("ImageProxyBundle already closed.");
- }
-
- // Returns the future that has been set if it exists
- ListenableFuture<ImageProxy> result = mFutureResults.get(captureId);
- if (result == null) {
- throw new IllegalArgumentException(
- "ImageProxyBundle does not contain this id: " + captureId);
- }
-
- return result;
- }
- }
-
- @Override
- @NonNull
- public List<Integer> getCaptureIds() {
- return Collections.unmodifiableList(mCaptureIdList);
- }
-
- /**
- * Add an {@link ImageProxy} to synchronize.
- */
- void addImageProxy(ImageProxy imageProxy) {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- Integer captureId =
- (Integer) imageProxy.getImageInfo().getTagBundle().getTag(mTagBundleKey);
- if (captureId == null) {
- throw new IllegalArgumentException("CaptureId is null.");
- }
-
- // If the CaptureId is associated with this SettableImageProxyBundle, set the
- // corresponding Future. Otherwise, throws exception.
- CallbackToFutureAdapter.Completer<ImageProxy> completer = mCompleters.get(captureId);
- if (completer != null) {
- mOwnedImageProxies.add(imageProxy);
- completer.set(imageProxy);
- } else {
- throw new IllegalArgumentException(
- "ImageProxyBundle does not contain this id: " + captureId);
- }
- }
- }
-
- /**
- * Flush all {@link ImageProxy} that have been added.
- */
- void close() {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
- for (ImageProxy imageProxy : mOwnedImageProxies) {
- imageProxy.close();
- }
- mOwnedImageProxies.clear();
- mFutureResults.clear();
- mCompleters.clear();
- mClosed = true;
- }
- }
-
- /**
- * Clear all {@link ImageProxy} that have been added and recreate the entries from the bundle.
- */
- void reset() {
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
- for (ImageProxy imageProxy : mOwnedImageProxies) {
- imageProxy.close();
- }
- mOwnedImageProxies.clear();
- mFutureResults.clear();
- mCompleters.clear();
- setup();
- }
- }
-
- private void setup() {
- synchronized (mLock) {
- for (final int captureId : mCaptureIdList) {
- ListenableFuture<ImageProxy> futureResult = CallbackToFutureAdapter.getFuture(
- new CallbackToFutureAdapter.Resolver<ImageProxy>() {
- @Override
- public Object attachCompleter(
- @NonNull CallbackToFutureAdapter.Completer<ImageProxy>
- completer) {
- synchronized (mLock) { // Not technically needed since
- // attachCompleter is called inline, but mLock is re-entrant
- // so there's no harm.
- mCompleters.put(captureId, completer);
- }
- return "getImageProxy(id: " + captureId + ")";
- }
- });
- mFutureResults.put(captureId, futureResult);
- }
- }
- }
-}
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 1ee0da0..a1fe196 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
@@ -37,10 +37,8 @@
* <p>Contains a {@link Surface} and its characteristics along with methods to manage the
* lifecycle of the {@link Surface}.
*
- * @hide
* @see SurfaceProcessor#onOutputSurface(SurfaceOutput)
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface SurfaceOutput {
/**
@@ -62,13 +60,7 @@
/**
* This field indicates that what purpose the {@link Surface} will be used for.
*
- * <ul>
- * <li>{@link SurfaceProcessor#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
- * <li>{@link SurfaceProcessor#VIDEO_CAPTURE} if the {@link Surface} will be used for video
- * capture.
- * <li>{@link SurfaceProcessor#PREVIEW} | {@link SurfaceProcessor#VIDEO_CAPTURE} if the output
- * {@link Surface} will be used for sharing a single stream for both preview and video capture.
- * </ul>
+ * <p>{@link CameraEffect#PREVIEW} if the {@link Surface} will be used for {@link Preview}.
*/
@CameraEffect.Targets
int getTargets();
@@ -81,37 +73,52 @@
/**
* Gets the format of the {@link Surface}.
+ *
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
int getFormat();
/**
- * Get the rotation degrees.
- */
- int getRotationDegrees();
-
- /**
* Call this method to mark the {@link Surface} as no longer in use.
*
- * <p>After this is called, the implementation should stop writing to the {@link Surface}
- * provided via {@link #getSurface}
+ * <p>Once the {@link SurfaceProcessor} implementation receives a request to close the
+ * {@link Surface}, it should call this method to acknowledge after stop writing to the
+ * {@link Surface}. Writing to the {@link Surface} after calling this method might cause
+ * errors.
*/
void close();
/**
- * Updates the 4 x 4 transformation matrix retrieved from {@link SurfaceTexture
- * #getTransformMatrix}.
+ * Updates the 4 x 4 transformation matrix retrieved from the input {@link Surface}.
*
- * <p>This method applies an additional transformation on top of the value of
- * {@link SurfaceTexture#getTransformMatrix}. The result is matrix of the same format, which
- * is a transform matrix maps 2D homogeneous texture coordinates of the form (s, t, 0, 1)
- * with s and t in the inclusive range [0, 1] to the texture coordinate that should be used
- * to sample that location from the texture. The matrix is stored in column-major order so that
- * it may be passed directly to OpenGL ES via the {@code glLoadMatrixf} or {@code
- * glUniformMatrix4fv} functions.
+ * <p>When the input {@link Surface} of {@link SurfaceProcessor} is backed by a
+ * {@link SurfaceTexture}, use this method to update the texture transform matrix.
+ *
+ * <p>After retrieving the transform matrix from {@link SurfaceTexture#getTransformMatrix},
+ * the {@link SurfaceProcessor} implementation should always call this method to update the
+ * value. The result is a matrix of the same format, which is a transform matrix maps 2D
+ * homogeneous texture coordinates of the form (s, t, 0, 1) with s and t in the inclusive
+ * range [0, 1] to the texture coordinate that should be used to sample that location from
+ * the texture. The matrix is stored in column-major order so that it may be passed directly
+ * to OpenGL ES via the {@code glLoadMatrixf} or {@code glUniformMatrix4fv} functions.
*
* <p>The additional transformation is calculated based on the target rotation, target
* resolution and the {@link ViewPort} associated with the target {@link UseCase}. The value
- * could also include workarounds for device specific quirks.
+ * could also include workarounds for device specific bugs. For example, correcting a
+ * stretched camera output stream.
+ *
+ * <p>Code sample:
+ * <pre><code>
+ * float[] transform = new float[16];
+ * float[] updatedTransform = new float[16];
+ *
+ * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+ * surfaceTexture.getTransformMatrix(transform);
+ * outputSurface.updateTransformMatrix(updatedTransform, transform);
+ * // Use the value of updatedTransform for OpenGL rendering.
+ * });
+ * </code></pre>
*
* @param updated the array into which the 4x4 matrix will be stored. The array must
* have exactly 16 elements.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
index 56e6aec..a622897 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceProcessor.java
@@ -20,20 +20,58 @@
import android.view.Surface;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.core.util.Consumer;
/**
* Interface to implement a GPU-based post-processing effect.
*
- * <p>This interface is for implementing a GPU effect for the {@link Preview} and/or
- * {@code VideoCapture} {@link UseCase}. Both the input and the output of the implementation
- * are {@link Surface}s. It's recommended to use graphics API such as OpenGL or Vulkan to access
- * the {@link Surface}.
+ * <p>This interface is for implementing a GPU effect for the {@link Preview} {@link UseCase}.
+ * Both the input and the output of the implementation are {@link Surface}s. It's recommended to
+ * use graphics API such as OpenGL or Vulkan to access the {@link Surface}.
*
- * @hide
+ * <p>If the implementation fails to process frames, for example, fails to allocate
+ * the resources, it should throw a {@link ProcessingException} in either {@link #onInputSurface} or
+ * {@link #onOutputSurface} to notify CameraX. If the implementation encounters an error after the
+ * pipeline is running, it should invalidate the input {@link Surface} by calling
+ * {@link SurfaceRequest#invalidate()}, then throwing a {@link ProcessingException} when
+ * {@link SurfaceProcessor#onInputSurface} is invoked again.
+ *
+ * <p>Once the implementation throws an exception, CameraX will treat it as an unrecoverable error
+ * and abort the pipeline. If the {@link SurfaceOutput#getTargets()} is
+ * {@link CameraEffect#PREVIEW}, CameraX will not propagate the error to the app. It's the
+ * implementation's responsibility to notify the app. For example:
+ *
+ * <pre><code>
+ * class SurfaceProcessorImpl implements SurfaceProcessor {
+ *
+ * Consumer<Exception> mErrorListener;
+ *
+ * SurfaceProcessorImpl(@NonNull Consumer<Exception> errorListener) {
+ * mErrorListener = errorListener;
+ * }
+ *
+ * void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ * try {
+ * // Setup the input stream.
+ * } catch (Exception e) {
+ * // Notify the app before throwing a ProcessingException.
+ * mErrorListener.accept(e)
+ * throw new ProcessingException(e);
+ * }
+ * }
+ *
+ * void onOutputSurface(@NonNull SurfaceRequest request) throws ProcessingException {
+ * try {
+ * // Setup the output streams.
+ * } catch (Exception e) {
+ * // Notify the app before throwing a ProcessingException.
+ * mErrorListener.accept(e)
+ * throw new ProcessingException(e);
+ * }
+ * }
+ * }
+ * </code></pre>
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface SurfaceProcessor {
/**
@@ -44,15 +82,44 @@
* listen for the {@link SurfaceTexture#setOnFrameAvailableListener} to get the incoming
* frames.
*
+ * <p>If the implementation encounters errors in creating the input {@link Surface}, it
+ * should throw an {@link ProcessingException} to notify CameraX.
+ *
+ * <p>The implementation can replace a previously provided {@link Surface} by invoking
+ * {@link SurfaceRequest#invalidate()}. Once invoked, CameraX will restart the camera
+ * pipeline and call {@link #onInputSurface} again with another {@link SurfaceRequest}.
+ *
* <p>The value of the {@link SurfaceTexture#getTransformMatrix} will need an additional
* transformation. CameraX calculates the additional transformation based on {@link UseCase}
* configurations such as {@link ViewPort} and target rotation, and provide the value via
* {@link SurfaceOutput#updateTransformMatrix(float[], float[])}.
*
+ * Code sample:
+ * <pre><code>
+ * // Create Surface based on the request.
+ * SurfaceTexture surfaceTexture = SurfaceTexture(textureName);
+ * surfaceTexture.setDefaultBufferSize(request.resolution.width, request.resolution.height);
+ * Surface surface = Surface(surfaceTexture);
+ *
+ * // Provide the Surface to CameraX, and cleanup when it's no longer used.
+ * surfaceRequest.provideSurface(surface, executor, result -> {
+ * surfaceTexture.setOnFrameAvailableListener(null)
+ * surfaceTexture.release()
+ * surface.release()
+ * });
+ *
+ * // Listen to the incoming frames.
+ * surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
+ * // Process the incoming frames and draw to the output Surface from #onOutputSurface
+ * }, handler);
+ * </code></pre>
+ *
* @param request a request to provide {@link Surface} for input.
+ * @throws ProcessingException if the implementation fails to fulfill the
+ * {@link SurfaceRequest}.
* @see SurfaceRequest
*/
- void onInputSurface(@NonNull SurfaceRequest request);
+ void onInputSurface(@NonNull SurfaceRequest request) throws ProcessingException;
/**
* Invoked when CameraX provides output Surface(s) for drawing processed frames.
@@ -63,12 +130,17 @@
* {@link Surface}. Then, the implementation should call {@link SurfaceOutput#close()} after it
* stops drawing to the {@link Surface}.
*
- * <p> When drawing to the {@link Surface}, the implementation should apply an additional
+ * <p>If the implementation encounters an error and cannot consume the {@link Surface},
+ * it should throw an {@link ProcessingException} to notify CameraX.
+ *
+ * <p>When drawing to the {@link Surface}, the implementation should apply an additional
* transformation to the input {@link Surface} by calling
* {@link SurfaceOutput#updateTransformMatrix(float[], float[])} with the value of
* {@link SurfaceTexture#getTransformMatrix(float[])}} from the input {@link Surface}.
*
* @param surfaceOutput contains a {@link Surface} for drawing processed frames.
+ * @throws ProcessingException if the implementation fails to consume the {@link SurfaceOutput}.
+ * @see SurfaceOutput
*/
- void onOutputSurface(@NonNull SurfaceOutput surfaceOutput);
+ void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) throws ProcessingException;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
index 88986d5..3137f24 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
@@ -71,8 +71,6 @@
@Nullable
private final Range<Integer> mExpectedFrameRate;
- private final boolean mRGBA8888Required;
-
private final CameraInternal mCamera;
// For the camera to retrieve the surface from the user
@@ -114,9 +112,8 @@
public SurfaceRequest(
@NonNull Size resolution,
@NonNull CameraInternal camera,
- boolean isRGBA8888Required,
@NonNull Runnable onInvalidated) {
- this(resolution, camera, isRGBA8888Required, /*expectedFrameRate=*/null, onInvalidated);
+ this(resolution, camera, /*expectedFrameRate=*/null, onInvalidated);
}
/**
@@ -129,13 +126,11 @@
public SurfaceRequest(
@NonNull Size resolution,
@NonNull CameraInternal camera,
- boolean isRGBA8888Required,
@Nullable Range<Integer> expectedFrameRate,
@NonNull Runnable onInvalidated) {
super();
mResolution = resolution;
mCamera = camera;
- mRGBA8888Required = isRGBA8888Required;
mExpectedFrameRate = expectedFrameRate;
// To ensure concurrency and ordering, operations are chained. Completion can only be
@@ -324,17 +319,6 @@
}
/**
- * Returns whether a surface of RGBA_8888 pixel format is required.
- *
- * @return true if a surface of RGBA_8888 pixel format is required.
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public boolean isRGBA8888Required() {
- return mRGBA8888Required;
- }
-
- /**
* Completes the request for a {@link Surface} if it has not already been
* completed or cancelled.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 5008b5c..e621821 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -26,7 +26,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
import androidx.lifecycle.Lifecycle;
import java.util.ArrayList;
@@ -78,10 +77,7 @@
/**
* Gets the {@link CameraEffect}s.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public List<CameraEffect> getEffects() {
return mEffects;
@@ -127,10 +123,7 @@
*
* <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
* the {@link UseCase}s.
- *
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@NonNull
public Builder addEffect(@NonNull CameraEffect cameraEffect) {
mEffects.add(cameraEffect);
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/CaptureProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureProcessor.java
deleted file mode 100644
index 38b82cf..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CaptureProcessor.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core.impl;
-
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Preview;
-import androidx.camera.core.impl.utils.futures.Futures;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-/**
- * A processing step of the image capture pipeline.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public interface CaptureProcessor {
- /**
- * This gets called to update where the CaptureProcessor should write the output of {@link
- * #process(ImageProxyBundle)}.
- *
- * @param surface The {@link Surface} that the CaptureProcessor should write data into.
- * @param imageFormat The format of that the surface expects.
- */
- void onOutputSurface(@NonNull Surface surface, int imageFormat);
-
- /**
- * Process a {@link ImageProxyBundle} for the set of captures that were
- * requested.
- *
- * <p> A result of the processing step must be written to the {@link Surface} that was
- * received by {@link #onOutputSurface(Surface, int)}. Otherwise, it might cause the
- * {@link ImageCapture#takePicture} can't be complete or frame lost in {@link Preview}.
- * @param bundle The set of images to process. The ImageProxyBundle and the {@link ImageProxy}
- * that are retrieved from it will become invalid after this method completes, so
- * no references to them should be kept.
- */
- void process(@NonNull ImageProxyBundle bundle);
-
- /**
- * This will be invoked when the input surface resolution is updated.
- *
- * @param size for the surface.
- */
- void onResolutionUpdate(@NonNull Size size);
-
- /**
- * Triggers to close the capture processor.
- *
- * <p>The capture processor might stop the in-progress task or have a flag to stop handling
- * new tasks after this function is called. When the capture processor is closed completely,
- * the {@link ListenableFuture} returned by {@link #getCloseFuture()} needs to be completed.
- */
- default void close() {
- // No-op by default.
- }
-
- /**
- * Returns the {@link ListenableFuture} which allows to know when the capture processor has
- * been closed completely.
- */
- @NonNull
- default ListenableFuture<Void> getCloseFuture() {
- // Returns immediate future by default.
- return Futures.immediateFuture(null);
- }
-}
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..b0ddbab 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) {
@@ -196,6 +207,17 @@
/**
* Checks if aspect ratio matches while tolerating rounding error.
*
+ * @see #isAspectRatioMatchingWithRoundingError(Size, boolean, Size, boolean)
+ */
+ public static boolean isAspectRatioMatchingWithRoundingError(
+ @NonNull Size size1, @NonNull Size size2) {
+ return isAspectRatioMatchingWithRoundingError(
+ size1, /*isAccurate1=*/ false, size2, /*isAccurate2=*/ false);
+ }
+
+ /**
+ * Checks if aspect ratio matches while tolerating rounding error.
+ *
* <p> One example of the usage is comparing the viewport-based crop rect from different use
* cases. The crop rect is rounded because pixels are integers, which may introduce an error
* when we check if the aspect ratio matches. For example, when
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java
deleted file mode 100644
index 12c9144..0000000
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/YuvToJpegProcessor.java
+++ /dev/null
@@ -1,305 +0,0 @@
-/*
- * Copyright 2020 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.internal;
-
-import android.graphics.ImageFormat;
-import android.graphics.Rect;
-import android.graphics.YuvImage;
-import android.media.Image;
-import android.media.ImageWriter;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.IntRange;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageOutputConfig;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.core.impl.utils.ExifData;
-import androidx.camera.core.impl.utils.ExifOutputStream;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.camera.core.internal.compat.ImageWriterCompat;
-import androidx.camera.core.internal.utils.ImageUtil;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.util.List;
-
-/**
- * A CaptureProcessor which produces JPEGs from input YUV images.
- */
-@RequiresApi(26)
-public class YuvToJpegProcessor implements CaptureProcessor {
- private static final String TAG = "YuvToJpegProcessor";
-
- private static final Rect UNINITIALIZED_RECT = new Rect(0, 0, 0, 0);
-
- private final int mMaxImages;
-
- private final Object mLock = new Object();
-
- @GuardedBy("mLock")
- @IntRange(from = 0, to = 100)
- private int mQuality;
- @GuardedBy("mLock")
- @ImageOutputConfig.RotationDegreesValue
- private int mRotationDegrees = 0;
-
- @GuardedBy("mLock")
- private boolean mClosed = false;
- @GuardedBy("mLock")
- private int mProcessingImages = 0;
- @GuardedBy("mLock")
- private ImageWriter mImageWriter;
- @GuardedBy("mLock")
- private Rect mImageRect = UNINITIALIZED_RECT;
-
- @GuardedBy("mLock")
- CallbackToFutureAdapter.Completer<Void> mCloseCompleter;
- @GuardedBy("mLock")
- private ListenableFuture<Void> mCloseFuture;
-
- public YuvToJpegProcessor(@IntRange(from = 0, to = 100) int quality, int maxImages) {
- mQuality = quality;
- mMaxImages = maxImages;
- }
-
- /**
- * Sets the compression quality for the output JPEG image.
- */
- public void setJpegQuality(@IntRange(from = 0, to = 100) int quality) {
- synchronized (mLock) {
- mQuality = quality;
- }
- }
-
- /**
- * Sets the rotation degrees value of the output images.
- *
- * @param rotationDegrees The rotation in degrees which will be a value in {0, 90, 180, 270}.
- */
- public void setRotationDegrees(@ImageOutputConfig.RotationDegreesValue int rotationDegrees) {
- synchronized (mLock) {
- mRotationDegrees = rotationDegrees;
- }
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- Preconditions.checkState(imageFormat == ImageFormat.JPEG, "YuvToJpegProcessor only "
- + "supports JPEG output format.");
- synchronized (mLock) {
- if (!mClosed) {
- if (mImageWriter != null) {
- throw new IllegalStateException("Output surface already set.");
- }
- mImageWriter = ImageWriterCompat.newInstance(surface, mMaxImages, imageFormat);
- } else {
- Logger.w(TAG, "Cannot set output surface. Processor is closed.");
- }
- }
- }
-
- @Override
- public void process(@NonNull ImageProxyBundle bundle) {
- List<Integer> ids = bundle.getCaptureIds();
- Preconditions.checkArgument(ids.size() == 1,
- "Processing image bundle have single capture id, but found " + ids.size());
-
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(ids.get(0));
- Preconditions.checkArgument(imageProxyListenableFuture.isDone());
-
- ImageWriter imageWriter;
- Rect imageRect;
- boolean processing;
- int quality;
- int rotationDegrees;
- synchronized (mLock) {
- imageWriter = mImageWriter;
- processing = !mClosed;
- imageRect = mImageRect;
- if (processing) {
- mProcessingImages++;
- }
- quality = mQuality;
- rotationDegrees = mRotationDegrees;
- }
-
- ImageProxy imageProxy = null;
- Image jpegImage = null;
- try {
- imageProxy = imageProxyListenableFuture.get();
- if (!processing) {
- Logger.w(TAG, "Image enqueued for processing on closed processor.");
- imageProxy.close();
- imageProxy = null;
- return;
- }
-
- jpegImage = imageWriter.dequeueInputImage();
-
- imageProxy = imageProxyListenableFuture.get();
- Preconditions.checkState(imageProxy.getFormat() == ImageFormat.YUV_420_888,
- "Input image is not expected YUV_420_888 image format");
- byte[] yuvBytes = ImageUtil.yuv_420_888toNv21(imageProxy);
-
- YuvImage yuvImage = new YuvImage(yuvBytes, ImageFormat.NV21, imageProxy.getWidth(),
- imageProxy.getHeight(), null);
-
- ByteBuffer jpegBuf = jpegImage.getPlanes()[0].getBuffer();
- int initialPos = jpegBuf.position();
- OutputStream os = new ExifOutputStream(new ByteBufferOutputStream(jpegBuf),
- ExifData.create(imageProxy, rotationDegrees));
- yuvImage.compressToJpeg(imageRect, quality, os);
-
- // Input can now be closed.
- imageProxy.close();
- imageProxy = null;
-
- // Set limits on jpeg buffer and rewind
- jpegBuf.limit(jpegBuf.position());
- jpegBuf.position(initialPos);
-
- // Enqueue the completed jpeg image
- imageWriter.queueInputImage(jpegImage);
- jpegImage = null;
- } catch (Exception e) {
- // InterruptedException, ExecutionException and EOFException might be caught here.
- //
- // InterruptedException should not be possible here since
- // imageProxyListenableFuture.isDone() returned true, but we have to handle the
- // exception case so bundle it with ExecutionException.
- //
- // EOFException might happen if the compressed JPEG data size exceeds the byte buffer
- // size of the output image reader.
- if (processing) {
- Logger.e(TAG, "Failed to process YUV -> JPEG", e);
- // Something went wrong attempting to retrieve ImageProxy. Enqueue an invalid buffer
- // to make sure the downstream isn't blocked.
- jpegImage = imageWriter.dequeueInputImage();
- ByteBuffer jpegBuf = jpegImage.getPlanes()[0].getBuffer();
- jpegBuf.rewind();
- jpegBuf.limit(0);
- imageWriter.queueInputImage(jpegImage);
- }
- } finally {
- boolean shouldCloseImageWriter;
- CallbackToFutureAdapter.Completer<Void> closeCompleter;
-
- synchronized (mLock) {
- // Note: order of condition is important here due to short circuit of &&
- shouldCloseImageWriter = processing && (mProcessingImages-- == 0) && mClosed;
- closeCompleter = mCloseCompleter;
- }
-
- // Fallback in case something went wrong during processing.
- if (jpegImage != null) {
- jpegImage.close();
- }
- if (imageProxy != null) {
- imageProxy.close();
- }
-
- if (shouldCloseImageWriter) {
- imageWriter.close();
- Logger.d(TAG, "Closed after completion of last image processed.");
-
- if (closeCompleter != null) {
- // Notify listeners of close
- closeCompleter.set(null);
- }
- }
- }
- }
-
- /**
- * Closes the YuvToJpegProcessor so that no more processing will occur.
- *
- * This should only be called once no more images will be produced for processing. Otherwise
- * the images may not be propagated to the output surface and the pipeline could stall.
- */
- @Override
- public void close() {
- CallbackToFutureAdapter.Completer<Void> closeCompleter = null;
-
- synchronized (mLock) {
- if (mClosed) {
- return;
- }
-
- mClosed = true;
- // Close the ImageWriter if no images are currently processing. Otherwise the
- // ImageWriter will be closed once the last image is closed.
- if (mProcessingImages == 0 && mImageWriter != null) {
- Logger.d(TAG, "No processing in progress. Closing immediately.");
- mImageWriter.close();
- closeCompleter = mCloseCompleter;
- } else {
- Logger.d(TAG, "close() called while processing. Will close after completion.");
- }
- }
-
- if (closeCompleter != null) {
- closeCompleter.set(null);
- }
- }
-
- /**
- * Returns a future that will complete when the YuvToJpegProcessor is actually closed.
- *
- * @return A future that signals when the YuvToJpegProcessor is actually closed
- * (after all processing). Cancelling this future has no effect.
- */
- @NonNull
- @Override
- public ListenableFuture<Void> getCloseFuture() {
- ListenableFuture<Void> closeFuture;
- synchronized (mLock) {
- if (mClosed && mProcessingImages == 0) {
- // Everything should be closed. Return immediate future.
- closeFuture = Futures.immediateFuture(null);
- } else {
- if (mCloseFuture == null) {
- mCloseFuture = CallbackToFutureAdapter.getFuture(completer -> {
- // Should already be locked, but lock again to satisfy linter.
- synchronized (mLock) {
- mCloseCompleter = completer;
- }
- return "YuvToJpegProcessor-close";
- });
- }
- closeFuture = Futures.nonCancellationPropagating(mCloseFuture);
- }
- }
- return closeFuture;
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- synchronized (mLock) {
- mImageRect = new Rect(0, 0, size.getWidth(), size.getHeight());
- }
- }
-}
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..c0b26c5 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}
*/
@@ -227,7 +233,7 @@
@Nullable Range<Integer> expectedFpsRange) {
checkMainThread();
// TODO(b/238230154) figure out how to support HDR.
- SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, false,
+ SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal,
expectedFpsRange, this::invalidate);
try {
setProvider(surfaceRequest.getDeferrableSurface());
@@ -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..508b3ff 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,14 +186,26 @@
return mFormat;
}
- /**
- * @inheritDoc
- */
- @Override
+ @VisibleForTesting
+ public Rect getInputCropRect() {
+ return mInputCropRect;
+ }
+
+ @VisibleForTesting
+ public Size getInputSize() {
+ return mInputSize;
+ }
+
+ @VisibleForTesting
public int getRotationDegrees() {
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 efaa57f..a72db62 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,33 +17,43 @@
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.isAspectRatioMatchingWithRoundingError;
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.
*
@@ -57,7 +67,10 @@
@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";
@NonNull
final SurfaceProcessorInternal mSurfaceProcessor;
@@ -65,9 +78,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}.
@@ -87,51 +100,54 @@
@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);
+ // The aspect ratio of the output must match the aspect ratio of the crop rect. Otherwise
+ // the output will be stretched.
+ Size rotatedCropSize = getRotatedSize(outConfig.getCropRect(), rotationDegrees);
+ checkArgument(isAspectRatioMatchingWithRoundingError(rotatedCropSize, outConfig.getSize()));
+
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);
@@ -140,28 +156,44 @@
}
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);
- mSurfaceProcessor.onOutputSurface(surfaceOutput);
- mSurfaceProcessor.onInputSurface(surfaceRequest);
+ public void onSuccess(@Nullable List<SurfaceOutput> outputs) {
+ Preconditions.checkNotNull(outputs);
+ try {
+ for (SurfaceOutput output : outputs) {
+ mSurfaceProcessor.onOutputSurface(output);
+ }
+ mSurfaceProcessor.onInputSurface(surfaceRequest);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+ }
}
@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());
@@ -179,13 +211,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 -> {
@@ -195,7 +227,10 @@
if (mirrored) {
rotationDegrees = -rotationDegrees;
}
- outputSurface.setRotationDegrees(within360(rotationDegrees));
+ rotationDegrees = within360(rotationDegrees);
+ for (SettableSurface output : outputSurfaces) {
+ output.setRotationDegrees(rotationDegrees);
+ }
});
}
@@ -206,12 +241,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/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
index 87a8644..3873031 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorWithExecutor.java
@@ -18,8 +18,13 @@
import static androidx.core.util.Preconditions.checkState;
+import android.os.Build;
+
import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+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;
@@ -33,8 +38,11 @@
* makes sure that CameraX always invoke the {@link SurfaceProcessor} on the correct
* {@link Executor}.
*/
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class SurfaceProcessorWithExecutor implements SurfaceProcessorInternal {
+ private static final String TAG = "SurfaceProcessor";
+
@NonNull
private final SurfaceProcessor mSurfaceProcessor;
@NonNull
@@ -63,12 +71,24 @@
@Override
public void onInputSurface(@NonNull SurfaceRequest request) {
- mExecutor.execute(() -> mSurfaceProcessor.onInputSurface(request));
+ mExecutor.execute(() -> {
+ try {
+ mSurfaceProcessor.onInputSurface(request);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor input.", e);
+ }
+ });
}
@Override
public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
- mExecutor.execute(() -> mSurfaceProcessor.onOutputSurface(surfaceOutput));
+ mExecutor.execute(() -> {
+ try {
+ mSurfaceProcessor.onOutputSurface(surfaceOutput);
+ } catch (ProcessingException e) {
+ Logger.e(TAG, "Failed to setup SurfaceProcessor output.", e);
+ }
+ });
}
@Override
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/ProcessingImageReaderTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java
deleted file mode 100644
index 386b37c..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/ProcessingImageReaderTest.java
+++ /dev/null
@@ -1,423 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static android.os.Looper.getMainLooper;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.graphics.ImageFormat;
-import android.os.Build;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.camera.core.impl.CaptureBundle;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.core.impl.ImageReaderProxy;
-import androidx.camera.core.impl.TagBundle;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
-import androidx.camera.testing.fakes.FakeCameraCaptureResult;
-import androidx.camera.testing.fakes.FakeCaptureStage;
-import androidx.camera.testing.fakes.FakeImageReaderProxy;
-import androidx.concurrent.futures.CallbackToFutureAdapter;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.android.util.concurrent.PausedExecutorService;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-
-// UnstableApiUsage is needed because PausedExecutorService is marked @Beta
-@SuppressWarnings({"UnstableApiUsage", "deprecation"})
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public final class ProcessingImageReaderTest {
- private static final int CAPTURE_ID_0 = 0;
- private static final int CAPTURE_ID_1 = 1;
- private static final int CAPTURE_ID_2 = 2;
- private static final int CAPTURE_ID_3 = 3;
- private static final long TIMESTAMP_0 = 0L;
- private static final long TIMESTAMP_1 = 1000L;
- private static final long TIMESTAMP_2 = 2000L;
- private static final long TIMESTAMP_3 = 4000L;
- private static final CaptureProcessor NOOP_PROCESSOR = new CaptureProcessor() {
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
-
- }
-
- @Override
- public void process(@NonNull ImageProxyBundle bundle) {
-
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
-
- }
- };
- private static PausedExecutorService sPausedExecutor;
- private final CaptureStage mCaptureStage0 = new FakeCaptureStage(CAPTURE_ID_0, null);
- private final CaptureStage mCaptureStage1 = new FakeCaptureStage(CAPTURE_ID_1, null);
- private final CaptureStage mCaptureStage2 = new FakeCaptureStage(CAPTURE_ID_2, null);
- private final CaptureStage mCaptureStage3 = new FakeCaptureStage(CAPTURE_ID_3, null);
- private final FakeImageReaderProxy mImageReaderProxy = new FakeImageReaderProxy(8);
- private MetadataImageReader mMetadataImageReader;
- private CaptureBundle mCaptureBundle;
- private String mTagBundleKey;
-
- @BeforeClass
- public static void setUpClass() {
- sPausedExecutor = new PausedExecutorService();
- }
-
- @AfterClass
- public static void tearDownClass() {
- sPausedExecutor.shutdown();
- }
-
- @Before
- public void setUp() {
- mCaptureBundle = CaptureBundles.createCaptureBundle(mCaptureStage0, mCaptureStage1);
- mTagBundleKey = Integer.toString(mCaptureBundle.hashCode());
- mMetadataImageReader = new MetadataImageReader(mImageReaderProxy);
- }
-
- @After
- public void cleanUp() {
- // Ensure the PausedExecutorService is drained
- sPausedExecutor.runAll();
- }
-
- @Test
- public void canSetFuturesInSettableImageProxyBundle()
- throws InterruptedException, TimeoutException, ExecutionException {
- // Sets the callback from ProcessingImageReader to start processing
- CaptureProcessor captureProcessor = mock(CaptureProcessor.class);
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- mMetadataImageReader, mCaptureBundle, captureProcessor).setPostProcessExecutor(
- sPausedExecutor).build();
- processingImageReader.setOnImageAvailableListener(mock(
- ImageReaderProxy.OnImageAvailableListener.class),
- CameraXExecutors.mainThreadExecutor());
- Map<Integer, Long> resultMap = new HashMap<>();
- resultMap.put(CAPTURE_ID_0, TIMESTAMP_0);
- resultMap.put(CAPTURE_ID_1, TIMESTAMP_1);
-
- // Cache current CaptureBundle as the TagBundle key for generate the fake image
- mTagBundleKey = processingImageReader.getTagBundleKey();
- triggerAndVerify(captureProcessor, resultMap);
- Mockito.reset(captureProcessor);
-
- CaptureBundle captureBundle = CaptureBundles.createCaptureBundle(mCaptureStage2,
- mCaptureStage3);
- processingImageReader.setCaptureBundle(captureBundle);
-
- // Reset the key for TagBundle because the CaptureBundle is renewed
- mTagBundleKey = processingImageReader.getTagBundleKey();
-
- Map<Integer, Long> resultMap1 = new HashMap<>();
- resultMap1.put(CAPTURE_ID_2, TIMESTAMP_2);
- resultMap1.put(CAPTURE_ID_3, TIMESTAMP_3);
- triggerAndVerify(captureProcessor, resultMap1);
- }
-
- private void triggerAndVerify(CaptureProcessor captureProcessor,
- Map<Integer, Long> captureIdToTime)
- throws InterruptedException, ExecutionException, TimeoutException {
- // Feeds ImageProxy with all capture id on the initial list.
- for (Integer id : captureIdToTime.keySet()) {
- triggerImageAvailable(id, captureIdToTime.get(id));
- }
-
- // Ensure tasks are posted to the processing executor
- shadowOf(getMainLooper()).idle();
-
- // Run processing
- sPausedExecutor.runAll();
-
- ArgumentCaptor<ImageProxyBundle> imageProxyBundleCaptor =
- ArgumentCaptor.forClass(ImageProxyBundle.class);
- verify(captureProcessor, times(1)).process(imageProxyBundleCaptor.capture());
- assertThat(imageProxyBundleCaptor.getValue()).isNotNull();
-
- // CaptureProcessor.process should be called once all ImageProxies on the
- // initial lists are ready. Then checks if the output has matched timestamp.
- for (Integer id : captureIdToTime.keySet()) {
- assertThat(imageProxyBundleCaptor.getValue().getImageProxy(id).get(0,
- TimeUnit.SECONDS).getImageInfo().getTimestamp()).isEqualTo(
- captureIdToTime.get(id));
- }
- }
-
- // Make sure that closing the ProcessingImageReader while the CaptureProcessor is processing
- // the image is safely done so that the CaptureProcessor will not be accessing closed images
- @Test
- public void canCloseWhileProcessingIsOccurring()
- throws InterruptedException {
- // Sets the callback from ProcessingImageReader to start processing
- WaitingCaptureProcessor waitingCaptureProcessor = new WaitingCaptureProcessor();
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- mMetadataImageReader, mCaptureBundle, waitingCaptureProcessor).build();
- processingImageReader.setOnImageAvailableListener(mock(
- ImageReaderProxy.OnImageAvailableListener.class),
- CameraXExecutors.mainThreadExecutor());
- Map<Integer, Long> resultMap = new HashMap<>();
- resultMap.put(CAPTURE_ID_0, TIMESTAMP_0);
- resultMap.put(CAPTURE_ID_1, TIMESTAMP_1);
-
- // Cache current CaptureBundle as the TagBundle key for generate the fake image
- mTagBundleKey = processingImageReader.getTagBundleKey();
-
- // Trigger the Images so that the CaptureProcessor starts
- for (Map.Entry<Integer, Long> idTimestamp : resultMap.entrySet()) {
- triggerImageAvailable(idTimestamp.getKey(), idTimestamp.getValue());
- }
-
- // Ensure tasks are posted to the processing executor
- shadowOf(getMainLooper()).idle();
-
- // Wait for CaptureProcessor.process() to start so that it is in the middle of processing
- assertThat(waitingCaptureProcessor.waitForProcessingToStart(3000)).isTrue();
-
- processingImageReader.close();
-
- // Allow the CaptureProcessor to continue processing. Calling finishProcessing() will
- // cause the CaptureProcessor to start accessing the ImageProxy. If the ImageProxy has
- // already been closed then we will time out at waitForProcessingToComplete().
- waitingCaptureProcessor.finishProcessing();
-
- // The processing will only complete if no exception was thrown during the processing
- // which causes it to return prematurely.
- assertThat(waitingCaptureProcessor.waitForProcessingToComplete(3000)).isTrue();
- }
-
- // Tests that a ProcessingImageReader can be closed while in the process of receiving
- // ImageProxies for an ImageProxyBundle.
- @Test
- public void closeImageHalfway() throws InterruptedException {
- // Sets the callback from ProcessingImageReader to start processing
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- mMetadataImageReader, mCaptureBundle, NOOP_PROCESSOR).setPostProcessExecutor(
- sPausedExecutor).build();
- processingImageReader.setOnImageAvailableListener(mock(
- ImageReaderProxy.OnImageAvailableListener.class),
- CameraXExecutors.mainThreadExecutor());
-
- // Cache current CaptureBundle as the TagBundle key for generate the fake image
- mTagBundleKey = processingImageReader.getTagBundleKey();
- triggerImageAvailable(CAPTURE_ID_0, TIMESTAMP_0);
-
- // Ensure the first image is received by the ProcessingImageReader
- shadowOf(getMainLooper()).idle();
-
- // The ProcessingImageReader is closed after receiving the first image, but before
- // receiving enough images for the entire ImageProxyBundle.
- processingImageReader.close();
-
- assertThat(mImageReaderProxy.isClosed()).isTrue();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void imageReaderSizeIsSmallerThanCaptureBundle() {
- // Creates a ProcessingImageReader with maximum Image number smaller than CaptureBundle
- // size.
- ImageReaderProxy imageReaderProxy = new FakeImageReaderProxy(1);
- MetadataImageReader metadataImageReader = new MetadataImageReader(imageReaderProxy);
-
- // Expects to throw exception when creating ProcessingImageReader.
- new ProcessingImageReader.Builder(metadataImageReader, mCaptureBundle,
- NOOP_PROCESSOR).build();
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void captureStageExceedMaxCaptureStage_setCaptureBundleThrowsException() {
- // Creates a ProcessingImageReader with maximum Image number.
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(100, 100,
- ImageFormat.YUV_420_888, 2, mCaptureBundle, mock(CaptureProcessor.class)).build();
-
- // Expects to throw exception when invoke the setCaptureBundle method with a
- // CaptureBundle size greater than maximum image number.
- processingImageReader.setCaptureBundle(
- CaptureBundles.createCaptureBundle(mCaptureStage1, mCaptureStage2, mCaptureStage3));
- }
-
- @Test
- public void imageReaderFormatIsOutputFormat() {
- // Creates a ProcessingImageReader with input format YUV_420_888 and output JPEG
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(100, 100,
- ImageFormat.YUV_420_888, 2, mCaptureBundle,
- mock(CaptureProcessor.class)).setOutputFormat(ImageFormat.JPEG).build();
-
- assertThat(processingImageReader.getImageFormat()).isEqualTo(ImageFormat.JPEG);
- }
-
- @Test
- public void canCloseUnderlyingCaptureProcessor() throws InterruptedException {
- // Sets up the underlying capture processor
- CaptureProcessor captureProcessor = mock(CaptureProcessor.class);
- AtomicReference<CallbackToFutureAdapter.Completer<Void>> underlyingCompleterReference =
- new AtomicReference<>();
- ListenableFuture<Void> underlyingCloseFuture =
- CallbackToFutureAdapter.getFuture(completer -> {
- underlyingCompleterReference.set(completer);
- return "underlyingCloseFuture";
- });
- when(captureProcessor.getCloseFuture()).thenReturn(underlyingCloseFuture);
- ProcessingImageReader processingImageReader = new ProcessingImageReader.Builder(
- mMetadataImageReader, mCaptureBundle, captureProcessor).build();
-
- // Calls the close() function of the ProcessingImageReader
- processingImageReader.close();
-
- // Verifies whether close() function of the underlying capture processor is called
- verify(captureProcessor, times(1)).close();
-
- // Sets up the listener to monitor whether the close future is closed or not.
- CountDownLatch closedLatch = new CountDownLatch(1);
- processingImageReader.getCloseFuture().addListener(() -> closedLatch.countDown(),
- CameraXExecutors.directExecutor());
-
- // Checks that the close future is not completed before the underlying capture processor
- // complete their close futures
- assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse();
-
- // Completes the completer of the underlying capture processor to complete their close
- // future
- underlyingCompleterReference.get().set(null);
-
- // Checks whether the close future of ProcessingImageReader is completed after the
- // underlying capture processor complete their close futures
- assertThat(closedLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
- }
-
- private void triggerImageAvailable(int captureId, long timestamp) throws InterruptedException {
- TagBundle tagBundle = TagBundle.create(new Pair<>(mTagBundleKey, captureId));
- mImageReaderProxy.triggerImageAvailable(tagBundle, timestamp);
- FakeCameraCaptureResult.Builder builder = new FakeCameraCaptureResult.Builder();
- builder.setTimestamp(timestamp);
- builder.setTag(tagBundle);
-
- mMetadataImageReader.getCameraCaptureCallback().onCaptureCompleted(builder.build());
- }
-
- // Only allows for processing once.
- private static class WaitingCaptureProcessor implements CaptureProcessor {
- // Block processing so that the ProcessingImageReader can be closed before the
- // CaptureProcessor has finished accessing the ImageProxy and ImageProxyBundle
- private final CountDownLatch mProcessingLatch = new CountDownLatch(1);
-
- // To wait for processing to start. This makes sure that the ProcessingImageReader can be
- // closed after processing has started
- private final CountDownLatch mProcessingStartLatch = new CountDownLatch(1);
-
- // Block processing from completing. This ensures that the CaptureProcessor has finished
- // accessing the ImageProxy and ImageProxyBundle successfully.
- private final CountDownLatch mProcessingComplete = new CountDownLatch(1);
-
- WaitingCaptureProcessor() {
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- }
-
- @Override
- public void process(@NonNull ImageProxyBundle bundle) {
- mProcessingStartLatch.countDown();
- try {
- mProcessingLatch.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- return;
- }
-
- ImageProxy imageProxy;
- try {
- imageProxy = bundle.getImageProxy(CAPTURE_ID_0).get();
- } catch (ExecutionException | InterruptedException e) {
- e.printStackTrace();
- return;
- }
-
- // Try to get the crop rect. If the image has already been closed it will thrown an
- // IllegalStateException
- try {
- imageProxy.getFormat();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- return;
- }
-
- mProcessingComplete.countDown();
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- }
-
- void finishProcessing() {
- mProcessingLatch.countDown();
- }
-
- /** Returns false if it fails to start processing. */
- boolean waitForProcessingToStart(long timeout) {
- try {
- return mProcessingStartLatch.await(timeout, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- return false;
- }
- }
-
- /** Returns false if processing does not complete. */
- boolean waitForProcessingToComplete(long timeout) {
- try {
- return mProcessingComplete.await(timeout, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- return false;
- }
- }
- }
-}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java
deleted file mode 100644
index 37fe524..0000000
--- a/camera/camera-core/src/test/java/androidx/camera/core/SettableImageProxyBundleTest.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.core;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.os.Build;
-
-import androidx.camera.testing.fakes.FakeImageInfo;
-import androidx.camera.testing.fakes.FakeImageProxy;
-
-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 java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class SettableImageProxyBundleTest {
- private static final int CAPTURE_ID_0 = 0;
- private static final int CAPTURE_ID_1 = 1;
- private static final int CAPTURE_ID_NONEXISTANT = 5;
- private static final long TIMESTAMP_0 = 10L;
- private static final long TIMESTAMP_1 = 20L;
- private final ImageInfo mImageInfo0 = new FakeImageInfo();
- private final ImageInfo mImageInfo1 = new FakeImageInfo();
- private ImageProxy mImageProxy0;
- private ImageProxy mImageProxy1;
- private List<Integer> mCaptureIdList;
- private SettableImageProxyBundle mImageProxyBundle;
- private String mTagBundleKey = "fakeTagBundleKey";
- @Before
- public void setup() {
- ((FakeImageInfo) mImageInfo0).setTimestamp(TIMESTAMP_0);
- ((FakeImageInfo) mImageInfo1).setTimestamp(TIMESTAMP_1);
- ((FakeImageInfo) mImageInfo0).setTag(mTagBundleKey, CAPTURE_ID_0);
- ((FakeImageInfo) mImageInfo1).setTag(mTagBundleKey, CAPTURE_ID_1);
- mImageProxy0 = new FakeImageProxy(mImageInfo0);
- mImageProxy1 = new FakeImageProxy(mImageInfo1);
-
- mCaptureIdList = new ArrayList<>();
- mCaptureIdList.add(CAPTURE_ID_0);
- mCaptureIdList.add(CAPTURE_ID_1);
-
- mImageProxyBundle = new SettableImageProxyBundle(mCaptureIdList, mTagBundleKey);
- }
-
- @Test
- public void canInvokeMatchedImageProxyFuture() throws InterruptedException,
- ExecutionException, TimeoutException {
-
- // Inputs two ImageProxy to SettableImageProxyBundle.
- mImageProxyBundle.addImageProxy(mImageProxy0);
- mImageProxyBundle.addImageProxy(mImageProxy1);
-
- // Tries to get the Images for the ListenableFutures got from SettableImageProxyBundle.
- ImageProxy result0 = mImageProxyBundle.getImageProxy(CAPTURE_ID_0).get(0, TimeUnit.SECONDS);
- ImageProxy result1 = mImageProxyBundle.getImageProxy(CAPTURE_ID_1).get(0, TimeUnit.SECONDS);
-
- // Checks if the results match what was input.
- assertThat(result0.getImageInfo()).isSameInstanceAs(mImageInfo0);
- assertThat(result0).isSameInstanceAs(mImageProxy0);
- assertThat(result1.getImageInfo()).isSameInstanceAs(mImageInfo1);
- assertThat(result1).isSameInstanceAs(mImageProxy1);
- }
-
- @Test
- public void canInvokeMatchedImageProxyFutureWithMultiTag() throws InterruptedException,
- ExecutionException, TimeoutException {
- FakeImageInfo imageInfo0 = new FakeImageInfo();
- imageInfo0.setTimestamp(TIMESTAMP_0);
- FakeImageInfo imageInfo1 = new FakeImageInfo();
- imageInfo1.setTimestamp(TIMESTAMP_1);
-
- imageInfo0.setTag(mTagBundleKey, CAPTURE_ID_0);
- imageInfo1.setTag(mTagBundleKey, CAPTURE_ID_1);
-
- ImageProxy imageProxy0;
- imageProxy0 = new FakeImageProxy(imageInfo0);
- ImageProxy imageProxy1;
- imageProxy1 = new FakeImageProxy(imageInfo1);
-
- mImageProxyBundle.addImageProxy(imageProxy0);
- mImageProxyBundle.addImageProxy(imageProxy1);
-
- // Tries to get the Images for the ListenableFutures got from SettableImageProxyBundle.
- ImageProxy result0 = mImageProxyBundle.getImageProxy(CAPTURE_ID_0).get(0, TimeUnit.SECONDS);
- ImageProxy result1 = mImageProxyBundle.getImageProxy(CAPTURE_ID_1).get(0, TimeUnit.SECONDS);
-
- // Checks if the results match what was input.
- assertThat(result0.getImageInfo()).isSameInstanceAs(imageInfo0);
- assertThat(result0).isSameInstanceAs(imageProxy0);
- assertThat(result1.getImageInfo()).isSameInstanceAs(imageInfo1);
- assertThat(result1).isSameInstanceAs(imageProxy1);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void exceptionWhenAddingImageWithInvalidCaptureId() {
- ImageInfo imageInfo = new FakeImageInfo();
- ImageProxy imageProxy = new FakeImageProxy(imageInfo);
-
- // Adds an ImageProxy with a capture id which doesn't exist in the initial list.
- ((FakeImageInfo) imageInfo).setTag(mTagBundleKey, CAPTURE_ID_NONEXISTANT);
- ((FakeImageProxy) imageProxy).setImageInfo(imageInfo);
-
- // Expects to throw exception while adding ImageProxy.
- mImageProxyBundle.addImageProxy(imageProxy);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void exceptionWhenRetrievingImageWithInvalidCaptureId() throws InterruptedException,
- ExecutionException, TimeoutException {
- // Tries to get a ImageProxy with non-existed capture id. Expects to throw exception
- // while getting ImageProxy.
- mImageProxyBundle.getImageProxy(CAPTURE_ID_NONEXISTANT).get(0, TimeUnit.SECONDS);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void exceptionWhenAddingImageWithInvalidCaptureIdInMultiTagCase() {
- ImageInfo imageInfo = new FakeImageInfo();
- ImageProxy imageProxy = new FakeImageProxy(imageInfo);
-
- // Adds an ImageProxy with a capture id which doesn't exist in the initial list.
- ((FakeImageInfo) imageInfo).setTag(mTagBundleKey,
- CAPTURE_ID_NONEXISTANT);
- ((FakeImageProxy) imageProxy).setImageInfo(imageInfo);
-
- // Expects to throw exception while adding ImageProxy.
- mImageProxyBundle.addImageProxy(imageProxy);
- }
-}
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..a5a6cde 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,15 @@
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.rotateSize
+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,79 +54,105 @@
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()
+ val videoOutputSize = rotateSize(VIDEO_SIZE, rotationDegrees - ROTATION_DEGREES)
createInputEdge(
- size = rectToSize(cropRect),
- cropRect = cropRect,
- rotationDegrees = rotationDegrees
+ previewRotationDegrees = rotationDegrees,
+ videoOutputSize = videoOutputSize
)
// 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(videoOutputSize)
+ assertThat(videoOutput.cropRect).isEqualTo(sizeToRect(videoOutputSize))
+ assertThat(videoOutput.rotationDegrees).isEqualTo(0)
// Clean up.
- inputEdge.surfaces[0].close()
+ nodeInput.surfaceEdge.close()
node.release()
+ shadowOf(getMainLooper()).idle()
}
}
+ @Test(expected = IllegalArgumentException::class)
+ fun cropSizeMismatchesOutputSize_throwsException() {
+ createSurfaceProcessorNode()
+ createInputEdge(
+ videoOutputSize = Size(VIDEO_SIZE.width - 2, VIDEO_SIZE.height + 2)
+ )
+ node.transform(nodeInput)
+ }
+
@Test
fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
for (mirroring in arrayOf(false, true)) {
@@ -132,15 +161,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 +177,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 +221,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 +238,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 +256,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 +266,41 @@
// 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,
+ videoOutputSize: Size = VIDEO_SIZE
) {
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,
+ videoOutputSize
+ )
+ previewOutConfig = OutConfig.of(surface)
+ nodeInput = SurfaceProcessorNode.In.of(
+ surface,
+ listOf(previewOutConfig, videoOutConfig)
+ )
}
private fun createSurfaceProcessorNode() {
@@ -251,15 +310,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-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
index 9047555..80aadbac 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
@@ -93,7 +93,7 @@
}
}, executor)
// Act: invoke methods.
- processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera(), false) {})
+ processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera()) {})
processorWithExecutor.onOutputSurface(mock(SurfaceOutput::class.java))
shadowOf(getMainLooper()).idle()
shadowOf(executorThread.looper).idle()
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureProcessor.java
deleted file mode 100644
index dd46bbc..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureProcessor.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import android.content.Context;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-import android.media.Image;
-import android.util.Pair;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
-import androidx.camera.core.ExperimentalGetImage;
-import androidx.camera.core.ImageInfo;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResults;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.extensions.impl.CaptureProcessorImpl;
-import androidx.camera.extensions.impl.ExtenderStateListener;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * A {@link CaptureProcessor} that calls a vendor provided implementation.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class AdaptingCaptureProcessor implements CaptureProcessor, VendorProcessor {
- @NonNull
- private final CaptureProcessorImpl mImpl;
- @Nullable
- private volatile Surface mSurface;
- private volatile int mImageFormat;
- private volatile Size mResolution;
-
- private final Object mLock = new Object();
-
- @GuardedBy("mLock")
- private boolean mActive = false;
-
- private BlockingCloseAccessCounter mAccessCounter = new BlockingCloseAccessCounter();
-
- public AdaptingCaptureProcessor(@NonNull CaptureProcessorImpl impl) {
- mImpl = impl;
- }
-
- /**
- * Invoked after
- * {@link ExtenderStateListener#onInit(String, CameraCharacteristics, Context)}()} to
- * initialize the processor.
- */
- @Override
- public void onInit() {
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- // Delay the onOutputSurface / onImageFormatUpdate/ onResolutionUpdate calls because on
- // some OEM devices, these CaptureProcessImpl configuration should be performed only after
- // onInit. Otherwise it will cause black preview issue.
- try {
- mImpl.onOutputSurface(mSurface, mImageFormat);
- mImpl.onImageFormatUpdate(mImageFormat);
- mImpl.onResolutionUpdate(mResolution);
- } finally {
- mAccessCounter.decrement();
- }
-
- synchronized (mLock) {
- mActive = true;
- }
- }
-
- @Override
- public void onDeInit() {
- synchronized (mLock) {
- mActive = false;
- }
- }
-
- @Override
- public void close() {
- mAccessCounter.destroyAndWaitForZeroAccess();
- mSurface = null;
- mResolution = null;
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- mSurface = surface;
- mImageFormat = imageFormat;
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- mResolution = size;
- }
-
- @Override
- @ExperimentalGetImage
- public void process(@NonNull ImageProxyBundle bundle) {
- synchronized (mLock) {
- if (!mActive) {
- return;
- }
-
- List<Integer> ids = bundle.getCaptureIds();
-
- Map<Integer, Pair<Image, TotalCaptureResult>> bundleMap = new HashMap<>();
-
- for (Integer id : ids) {
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(id);
- try {
- ImageProxy imageProxy = imageProxyListenableFuture.get(5, TimeUnit.SECONDS);
- Image image = imageProxy.getImage();
- if (image == null) {
- return;
- }
-
- ImageInfo imageInfo = imageProxy.getImageInfo();
-
- CameraCaptureResult result =
- CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
- if (result == null) {
- return;
- }
-
- CaptureResult captureResult =
- Camera2CameraCaptureResultConverter.getCaptureResult(result);
- if (captureResult == null) {
- return;
- }
-
- TotalCaptureResult totalCaptureResult = (TotalCaptureResult) captureResult;
- if (totalCaptureResult == null) {
- return;
- }
-
- Pair<Image, TotalCaptureResult> imageCapturePair = new Pair<>(
- imageProxy.getImage(), totalCaptureResult);
- bundleMap.put(id, imageCapturePair);
- } catch (TimeoutException | ExecutionException | InterruptedException e) {
- return;
- }
- }
-
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- try {
- mImpl.process(bundleMap);
- } finally {
- mAccessCounter.decrement();
- }
- }
- }
-}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
deleted file mode 100644
index f9bc984..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import android.hardware.camera2.CaptureRequest;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.OptIn;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2ImplConfig;
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
-import androidx.camera.core.impl.CaptureConfig;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-
-/** A {@link CaptureStage} that calls a vendor provided implementation. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class AdaptingCaptureStage implements CaptureStage {
-
- private final CaptureConfig mCaptureRequestConfiguration;
- private final int mId;
-
- @SuppressWarnings("unchecked")
- @OptIn(markerClass = ExperimentalCamera2Interop.class)
- public AdaptingCaptureStage(@NonNull CaptureStageImpl impl) {
- mId = impl.getId();
- Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();
-
- for (Pair<CaptureRequest.Key, Object> captureParameter : impl.getParameters()) {
- camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,
- captureParameter.second);
- }
-
- CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
- captureConfigBuilder.addImplementationOptions(camera2ConfigurationBuilder.build());
- mCaptureRequestConfiguration = captureConfigBuilder.build();
- }
-
- @Override
- public int getId() {
- return mId;
- }
-
- @Override
- @NonNull
- public CaptureConfig getCaptureConfig() {
- return mCaptureRequestConfiguration;
- }
-}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingPreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingPreviewProcessor.java
deleted file mode 100644
index e3fda54..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingPreviewProcessor.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import android.content.Context;
-import android.graphics.ImageFormat;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-import android.media.Image;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
-import androidx.camera.core.ExperimentalGetImage;
-import androidx.camera.core.ImageInfo;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResults;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.extensions.impl.ExtenderStateListener;
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-/**
- * A {@link CaptureProcessor} that calls a vendor provided preview processing implementation.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class AdaptingPreviewProcessor implements CaptureProcessor, VendorProcessor {
- private static final String TAG = "AdaptingPreviewProcesso";
- private final PreviewImageProcessorImpl mImpl;
- private volatile Surface mSurface;
- private volatile int mImageFormat;
- private volatile Size mResolution;
-
- private final Object mLock = new Object();
-
- @GuardedBy("mLock")
- private boolean mActive = false;
-
- /**
- * Invoked after
- * {@link ExtenderStateListener#onInit(String, CameraCharacteristics, Context)}()} to
- * initialize the processor.
- */
- @Override
- public void onInit() {
- // Delay the onOutputSurface / onImageFormatUpdate/ onResolutionUpdate calls because on
- // some OEM devices, these CaptureProcessImpl configuration should be performed only after
- // onInit. Otherwise it will cause black preview issue.
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- try {
- mImpl.onResolutionUpdate(mResolution);
- mImpl.onOutputSurface(mSurface, mImageFormat);
- // No input formats other than YUV_420_888 are allowed.
- mImpl.onImageFormatUpdate(ImageFormat.YUV_420_888);
- } finally {
- mAccessCounter.decrement();
- }
-
- synchronized (mLock) {
- mActive = true;
- }
- }
-
- private BlockingCloseAccessCounter mAccessCounter = new BlockingCloseAccessCounter();
-
- public AdaptingPreviewProcessor(@NonNull PreviewImageProcessorImpl impl) {
- mImpl = impl;
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- try {
- mSurface = surface;
- mImageFormat = imageFormat;
- } finally {
- mAccessCounter.decrement();
- }
- }
-
- @ExperimentalGetImage
- @Override
- public void process(@NonNull ImageProxyBundle bundle) {
- synchronized (mLock) {
- if (!mActive) {
- return;
- }
-
- List<Integer> ids = bundle.getCaptureIds();
- Preconditions.checkArgument(ids.size() == 1,
- "Processing preview bundle must be 1, but found " + ids.size());
-
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(
- ids.get(0));
- Preconditions.checkArgument(imageProxyListenableFuture.isDone());
-
- ImageProxy imageProxy;
- try {
- imageProxy = imageProxyListenableFuture.get();
- } catch (ExecutionException | InterruptedException e) {
- Logger.e(TAG, "Unable to retrieve ImageProxy from bundle");
- return;
- }
-
- Image image = imageProxy.getImage();
-
- ImageInfo imageInfo = imageProxy.getImageInfo();
- CameraCaptureResult result =
- CameraCaptureResults.retrieveCameraCaptureResult(imageInfo);
- CaptureResult captureResult =
- Camera2CameraCaptureResultConverter.getCaptureResult(result);
-
- TotalCaptureResult totalCaptureResult = null;
- if (captureResult instanceof TotalCaptureResult) {
- totalCaptureResult = (TotalCaptureResult) captureResult;
- }
-
- if (image == null) {
- return;
- }
-
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- try {
- mImpl.process(image, totalCaptureResult);
- } finally {
- mAccessCounter.decrement();
- }
- }
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- if (!mAccessCounter.tryIncrement()) {
- return;
- }
-
- try {
- mResolution = size;
- } finally {
- mAccessCounter.decrement();
- }
- }
-
- @Override
- public void onDeInit() {
- synchronized (mLock) {
- mActive = false;
- }
- }
-
- @Override
- public void close() {
- mAccessCounter.destroyAndWaitForZeroAccess();
- mSurface = null;
- mResolution = null;
- }
-}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessor.java
deleted file mode 100644
index f10a4aa..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessor.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
-import androidx.camera.core.ImageInfo;
-import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResults;
-import androidx.camera.core.impl.CaptureStage;
-import androidx.camera.core.impl.ImageInfoProcessor;
-import androidx.camera.extensions.impl.CaptureStageImpl;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
-import androidx.core.util.Preconditions;
-
-/**
- * A {@link ImageInfoProcessor} that calls a vendor provided preview processing implementation.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public final class AdaptingRequestUpdateProcessor implements ImageInfoProcessor, VendorProcessor {
- private final PreviewExtenderImpl mPreviewExtenderImpl;
- private final RequestUpdateProcessorImpl mProcessorImpl;
- private BlockingCloseAccessCounter mAccessCounter = new BlockingCloseAccessCounter();
-
- public AdaptingRequestUpdateProcessor(@NonNull PreviewExtenderImpl previewExtenderImpl) {
- Preconditions.checkArgument(previewExtenderImpl.getProcessorType()
- == PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY,
- "AdaptingRequestUpdateProcess can only adapt extender with "
- + "PROCESSOR_TYPE_REQUEST_UPDATE_ONLY ProcessorType.");
- mPreviewExtenderImpl = previewExtenderImpl;
- mProcessorImpl = (RequestUpdateProcessorImpl) mPreviewExtenderImpl.getProcessor();
- }
-
- @Override
- @Nullable
- public CaptureStage getCaptureStage() {
- if (!mAccessCounter.tryIncrement()) {
- return null;
- }
-
- try {
- return new AdaptingCaptureStage(mPreviewExtenderImpl.getCaptureStage());
- } finally {
- mAccessCounter.decrement();
- }
-
- }
-
- @Override
- public boolean process(@NonNull ImageInfo imageInfo) {
- if (!mAccessCounter.tryIncrement()) {
- return false;
- }
-
- try {
- boolean processResult = false;
-
- CameraCaptureResult result = CameraCaptureResults.retrieveCameraCaptureResult(
- imageInfo);
- CaptureResult captureResult = Camera2CameraCaptureResultConverter.getCaptureResult(
- result);
-
- if (captureResult instanceof TotalCaptureResult) {
-
- CaptureStageImpl captureStageImpl =
- mProcessorImpl.process((TotalCaptureResult) captureResult);
- processResult = captureStageImpl != null;
- }
- return processResult;
- } finally {
- mAccessCounter.decrement();
- }
- }
-
- @Override
- public void close() {
- mAccessCounter.destroyAndWaitForZeroAccess();
- }
-}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
index f419244..145105e 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BasicVendorExtender.java
@@ -20,7 +20,6 @@
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.params.StreamConfigurationMap;
-import android.os.Build;
import android.util.Pair;
import android.util.Range;
import android.util.Size;
@@ -289,11 +288,7 @@
@Override
public SessionProcessor createSessionProcessor(@NonNull Context context) {
Preconditions.checkNotNull(mCameraInfo, "VendorExtender#init() must be called first");
- if (Build.VERSION.SDK_INT >= 26) {
- return new BasicExtenderSessionProcessor(
- mPreviewExtenderImpl, mImageCaptureExtenderImpl, context);
- } else {
- throw new IllegalArgumentException("SessionProcessor is not supported");
- }
+ return new BasicExtenderSessionProcessor(mPreviewExtenderImpl, mImageCaptureExtenderImpl,
+ context);
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BlockingCloseAccessCounter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BlockingCloseAccessCounter.java
deleted file mode 100644
index b41659e..0000000
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/BlockingCloseAccessCounter.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import androidx.annotation.GuardedBy;
-import androidx.annotation.RequiresApi;
-
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * A counter for blocking closing until all access to the counter has been completed.
- *
- * <pre>{@code
- * public void callingMethod() {
- * if (!mAtomicAccessCounter.tryIncrement()) {
- * return;
- * }
- *
- * try {
- * // Some work that needs to be done
- * } finally {
- * mAtomicAccessCounter.decrement();
- * }
- * }
- *
- * // Method that can only be called after all callingMethods are done with access
- * public void blockingMethod() {
- * destroyAndWaitForZeroAccess();
- * }
- * }</pre>
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-final class BlockingCloseAccessCounter {
- @GuardedBy("mLock")
- private AtomicInteger mAccessCount = new AtomicInteger(0);
- private final Lock mLock = new ReentrantLock();
- private final Condition mDoneCondition = mLock.newCondition();
-
- private static final int COUNTER_DESTROYED_FLAG = -1;
-
- /**
- * Attempt to increment the access counter.
- *
- * <p>Once {@link #destroyAndWaitForZeroAccess()} has returned this will always fail to
- * increment, meaning access is not safe.
- *
- * @return true if the counter was incremented, false otherwise
- */
- boolean tryIncrement() {
- mLock.lock();
- try {
- if (mAccessCount.get() == COUNTER_DESTROYED_FLAG) {
- return false;
- }
- mAccessCount.getAndIncrement();
- } finally {
- mLock.unlock();
- }
- return true;
- }
-
- /**
- * Decrement the access counter.
- **/
- void decrement() {
- mLock.lock();
- try {
- switch (mAccessCount.getAndDecrement()) {
- case COUNTER_DESTROYED_FLAG:
- throw new IllegalStateException("Unable to decrement. Counter already "
- + "destroyed");
- case 0:
- throw new IllegalStateException("Unable to decrement. No corresponding "
- + "counter increment");
- default:
- //
- }
- mDoneCondition.signal();
- } finally {
- mLock.unlock();
- }
- }
-
- /**
- * Blocks until there are zero accesses in the counter.
- *
- * <p>Once this call completes, the counter is destroyed and can not be incremented and
- * decremented.
- */
- void destroyAndWaitForZeroAccess() {
- mLock.lock();
-
- try {
- while (!mAccessCount.compareAndSet(0, COUNTER_DESTROYED_FLAG)) {
- try {
- mDoneCondition.await();
- } catch (InterruptedException e) {
- // Continue to check
- }
- }
- } finally {
- mLock.unlock();
- }
- }
-}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
index 74de26b..5049106 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessor.java
@@ -62,7 +62,7 @@
* A {@link SessionProcessor} based on OEMs' basic extender implementation.
*/
@OptIn(markerClass = ExperimentalCamera2Interop.class)
-@RequiresApi(26) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class BasicExtenderSessionProcessor extends SessionProcessorBase {
private static final String TAG = "BasicSessionProcessor";
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
index 0f34acb..1e8acf7 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/PreviewProcessor.java
@@ -39,7 +39,7 @@
*
* <p>Please note that output preview surface must be closed AFTER this processor is closed.
*/
-@RequiresApi(26)
+@RequiresApi(21)
class PreviewProcessor {
private static final String TAG = "PreviewProcessor";
@NonNull
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
index d3f47fa..b1d702f 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/sessionprocessor/StillCaptureProcessor.java
@@ -61,7 +61,7 @@
* Please note that the output JPEG surface should be closed AFTER this processor is closed().
* </pre>
*/
-@RequiresApi(26)
+@RequiresApi(21)
class StillCaptureProcessor {
private static final String TAG = "StillCaptureProcessor";
private static final int MAX_IMAGES = 2;
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingCaptureProcessorTest.kt b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingCaptureProcessorTest.kt
deleted file mode 100644
index 54cb68d..0000000
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingCaptureProcessorTest.kt
+++ /dev/null
@@ -1,116 +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.extensions.internal
-
-import android.hardware.camera2.TotalCaptureResult
-import android.media.Image
-import android.os.Build
-import android.util.Pair
-import android.util.Size
-import android.view.Surface
-import androidx.camera.camera2.internal.Camera2CameraCaptureResult
-import androidx.camera.core.impl.ImageProxyBundle
-import androidx.camera.core.impl.SingleImageProxyBundle
-import androidx.camera.core.impl.TagBundle
-import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.extensions.impl.CaptureProcessorImpl
-import androidx.camera.testing.fakes.FakeImageProxy
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.Mockito
-import org.mockito.Mockito.inOrder
-import org.robolectric.RobolectricTestRunner
-import org.robolectric.annotation.Config
-import org.robolectric.annotation.internal.DoNotInstrument
-
-private const val TAG_BUNDLE_KEY = "FakeTagBundleKey"
-private const val CAPTURE_ID = 0
-
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class AdaptingCaptureProcessorTest {
- private val captureProcessorImpl = Mockito.mock(CaptureProcessorImpl::class.java)
- private var adaptingCaptureProcessor = AdaptingCaptureProcessor(captureProcessorImpl)
- private val imageProxyBundle = createFakeImageProxyBundle()
-
- @Test
- fun processDoesNotCallImplAfterClose() {
- callOnInitAndVerify()
- adaptingCaptureProcessor.close()
- adaptingCaptureProcessor.process(imageProxyBundle)
- Mockito.verifyNoMoreInteractions(captureProcessorImpl)
- }
-
- @Test
- fun onImageFormatUpdateDoesNotCallImplAfterClose() {
- adaptingCaptureProcessor.close()
- adaptingCaptureProcessor.onOutputSurface(Mockito.mock(Surface::class.java), 0)
- adaptingCaptureProcessor.onInit()
- Mockito.verifyNoMoreInteractions(captureProcessorImpl)
- }
-
- @Test
- fun onResolutionUpdateDoesNotCallImplAfterClose() {
- adaptingCaptureProcessor.close()
- adaptingCaptureProcessor.onResolutionUpdate(Size(640, 480))
- adaptingCaptureProcessor.onInit()
- Mockito.verifyNoMoreInteractions(captureProcessorImpl)
- }
-
- @Test
- fun processCanCallImplBeforeDeInit() {
- callOnInitAndVerify()
- adaptingCaptureProcessor.process(imageProxyBundle)
- Mockito.verify(captureProcessorImpl, Mockito.times(1)).process(any())
- }
-
- @Test
- fun processDoesNotCallImplAfterDeInit() {
- callOnInitAndVerify()
- adaptingCaptureProcessor.onDeInit()
- adaptingCaptureProcessor.process(imageProxyBundle)
- Mockito.verifyNoMoreInteractions(captureProcessorImpl)
- }
-
- private fun createFakeImageProxyBundle(
- bundleKey: String = TAG_BUNDLE_KEY,
- captureId: Int = CAPTURE_ID
- ): ImageProxyBundle {
- val fakeCameraCaptureResult = Mockito.mock(Camera2CameraCaptureResult::class.java)
- Mockito.`when`(fakeCameraCaptureResult.tagBundle)
- .thenReturn(TagBundle.create(Pair.create(bundleKey, captureId)))
- Mockito.`when`(fakeCameraCaptureResult.captureResult)
- .thenReturn(Mockito.mock(TotalCaptureResult::class.java))
- val fakeImageInfo = CameraCaptureResultImageInfo(fakeCameraCaptureResult)
- val fakeImageProxy = FakeImageProxy(fakeImageInfo)
- fakeImageProxy.image = Mockito.mock(Image::class.java)
- return SingleImageProxyBundle(fakeImageProxy, bundleKey)
- }
-
- private fun callOnInitAndVerify() {
- adaptingCaptureProcessor.onInit()
-
- inOrder(captureProcessorImpl).apply {
- verify(captureProcessorImpl).onOutputSurface(any(), anyInt())
- verify(captureProcessorImpl).onImageFormatUpdate(anyInt())
- verify(captureProcessorImpl).onResolutionUpdate(any())
- }
- }
-}
\ No newline at end of file
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingPreviewProcessorTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingPreviewProcessorTest.java
deleted file mode 100644
index ba9d15b..0000000
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingPreviewProcessorTest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import android.media.Image;
-import android.os.Build;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.camera.core.impl.SingleImageProxyBundle;
-import androidx.camera.extensions.impl.PreviewImageProcessorImpl;
-import androidx.camera.testing.fakes.FakeImageInfo;
-import androidx.camera.testing.fakes.FakeImageProxy;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.InOrder;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class AdaptingPreviewProcessorTest {
- private AdaptingPreviewProcessor mAdaptingPreviewProcessor;
- private PreviewImageProcessorImpl mImpl;
- private ImageProxyBundle mImageProxyBundle;
- private String mTagBundleKey = "FakeTagBundleKey";
-
- @Before
- public void setup() {
- mImpl = mock(PreviewImageProcessorImpl.class);
-
- FakeImageInfo fakeImageInfo = new FakeImageInfo();
- // Use the key which SingleImageProxyBundle is used to get tag.
- fakeImageInfo.setTag(mTagBundleKey, 1);
-
- FakeImageProxy fakeImageProxy = new FakeImageProxy(fakeImageInfo);
- fakeImageProxy.setImage(mock(Image.class));
-
- mImageProxyBundle = new SingleImageProxyBundle(fakeImageProxy, mTagBundleKey);
- mAdaptingPreviewProcessor = new AdaptingPreviewProcessor(mImpl);
- }
-
- @Test
- public void processDoesNotCallImplAfterClose() {
- mAdaptingPreviewProcessor.close();
- mAdaptingPreviewProcessor.process(mImageProxyBundle);
- mAdaptingPreviewProcessor.onInit();
-
- verifyNoMoreInteractions(mImpl);
- }
-
- @Test
- public void onImageFormatUpdateDoesNotCallImplAfterClose() {
- mAdaptingPreviewProcessor.close();
- mAdaptingPreviewProcessor.onOutputSurface(mock(Surface.class), 0);
- mAdaptingPreviewProcessor.onInit();
-
- verifyNoMoreInteractions(mImpl);
- }
-
- @Test
- public void onResolutionUpdateDoesNotCallImplAfterClose() {
- mAdaptingPreviewProcessor.close();
- mAdaptingPreviewProcessor.onResolutionUpdate(new Size(640, 480));
- mAdaptingPreviewProcessor.onInit();
-
- verifyNoMoreInteractions(mImpl);
- }
-
- @Test
- public void processCanCallImplBeforeDeInit() {
- callOnInitAndVerify();
- mAdaptingPreviewProcessor.process(mImageProxyBundle);
- verify(mImpl, times(1)).process(any(), any());
- }
-
- @Test
- public void processDoesNotCallImplAfterDeInit() {
- callOnInitAndVerify();
- mAdaptingPreviewProcessor.onDeInit();
- mAdaptingPreviewProcessor.process(mImageProxyBundle);
-
- verifyNoMoreInteractions(mImpl);
- }
-
- private void callOnInitAndVerify() {
- mAdaptingPreviewProcessor.onInit();
-
- InOrder inOrderImpl = inOrder(mImpl);
- inOrderImpl.verify(mImpl).onResolutionUpdate(any());
- inOrderImpl.verify(mImpl).onOutputSurface(any(), anyInt());
- inOrderImpl.verify(mImpl).onImageFormatUpdate(anyInt());
- }
-}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessorTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessorTest.java
deleted file mode 100644
index ff8b784..0000000
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/AdaptingRequestUpdateProcessorTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
-
-import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.TotalCaptureResult;
-import android.os.Build;
-
-import androidx.camera.camera2.impl.Camera2CameraCaptureResultConverter;
-import androidx.camera.core.ImageInfo;
-import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResults;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.impl.RequestUpdateProcessorImpl;
-
-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.Implementation;
-import org.robolectric.annotation.Implements;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP,
- shadows = {
- AdaptingRequestUpdateProcessorTest.ShadowCameraCaptureResults.class,
- AdaptingRequestUpdateProcessorTest.ShadowCamera2CameraCaptureResultConverter.class})
-public class AdaptingRequestUpdateProcessorTest {
- private AdaptingRequestUpdateProcessor mAdaptingRequestUpdateProcessor;
- private PreviewExtenderImpl mPreviewExtenderImpl;
- private RequestUpdateProcessorImpl mImpl;
- private ImageInfo mImageInfo;
-
- @Before
- public void setup() {
- mImpl = mock(RequestUpdateProcessorImpl.class);
- mPreviewExtenderImpl = mock(PreviewExtenderImpl.class);
- when(mPreviewExtenderImpl.getProcessor()).thenReturn(mImpl);
- when(mPreviewExtenderImpl.getProcessorType()).thenReturn(
- PreviewExtenderImpl.ProcessorType.PROCESSOR_TYPE_REQUEST_UPDATE_ONLY);
-
- mImageInfo = mock(ImageInfo.class);
-
- mAdaptingRequestUpdateProcessor = new AdaptingRequestUpdateProcessor(mPreviewExtenderImpl);
- }
-
- @Test
- public void getCaptureStageDoesNotCallImplAfterClose() {
- clearInvocations(mPreviewExtenderImpl);
- mAdaptingRequestUpdateProcessor.close();
-
- mAdaptingRequestUpdateProcessor.getCaptureStage();
-
- verifyNoMoreInteractions(mPreviewExtenderImpl);
- }
-
- @Test
- public void processDoesNotCallImplAfterClose() {
- mAdaptingRequestUpdateProcessor.close();
-
- mAdaptingRequestUpdateProcessor.process(mImageInfo);
-
- verifyNoMoreInteractions(mImpl);
- }
-
- /**
- * Shadow of {@link Camera2CameraCaptureResultConverter} to control return of
- * {@link #getCaptureResult(CameraCaptureResult)}.
- */
- @Implements(
- value = Camera2CameraCaptureResultConverter.class,
- minSdk = 21
- )
- static final class ShadowCamera2CameraCaptureResultConverter {
- /** Returns {@link TotalCaptureResult} regardless of input. */
- @Implementation
- public static CaptureResult getCaptureResult(CameraCaptureResult cameraCaptureResult) {
- return mock(TotalCaptureResult.class);
- }
- }
-
- /**
- * Shadow of {@link CameraCaptureResults} to control return of
- * {@link #retrieveCameraCaptureResult(ImageInfo)}.
- */
- @Implements(
- value = CameraCaptureResults.class,
- minSdk = 21
- )
- static final class ShadowCameraCaptureResults {
- /** Returns {@link CameraCaptureResult} regardless of input. */
- @Implementation
- public static CameraCaptureResult retrieveCameraCaptureResult(ImageInfo imageInfo) {
- return mock(CameraCaptureResult.class);
- }
- }
-
-}
diff --git a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/BlockingCloseAccessCounterTest.java b/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/BlockingCloseAccessCounterTest.java
deleted file mode 100644
index ececd6d..0000000
--- a/camera/camera-extensions/src/test/java/androidx/camera/extensions/internal/BlockingCloseAccessCounterTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2020 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.extensions.internal;
-
-import static org.junit.Assert.assertFalse;
-
-import android.os.Build;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.robolectric.RobolectricTestRunner;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-
-@RunWith(RobolectricTestRunner.class)
-@DoNotInstrument
-@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class BlockingCloseAccessCounterTest {
- @Test(expected = IllegalStateException.class)
- public void decrementWithoutIncrementThrowsException() {
- BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
-
- // Expect a IllegalStateException to be thrown
- counter.decrement();
- }
-
- @Test(expected = IllegalStateException.class)
- public void decrementAfterDestroy() {
- BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
- counter.destroyAndWaitForZeroAccess();
-
- // Expect a IllegalStateException to be thrown
- counter.decrement();
- }
-
- @Test
- public void incrementAfterDestroyDoesNotIncrement() {
- BlockingCloseAccessCounter counter = new BlockingCloseAccessCounter();
- counter.destroyAndWaitForZeroAccess();
-
- assertFalse(counter.tryIncrement());
- }
-}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/TimestampCaptureProcessor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/TimestampCaptureProcessor.java
deleted file mode 100644
index 72c18fa..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/TimestampCaptureProcessor.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.testing;
-
-import android.media.Image;
-import android.util.Size;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.camera.core.ExperimentalGetImage;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.impl.CaptureProcessor;
-import androidx.camera.core.impl.ImageProxyBundle;
-import androidx.core.util.Preconditions;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-
-/**
- * A {@link CaptureProcessor} that wraps another CaptureProcessor and captures the timestamps of all
- * the {@link ImageProxy} that are processed by it.
- *
- * <p>This class is used for testing of preview processing only. The expectation is that each
- * {@link ImageProxyBundle} will only have a single {@link ImageProxy}.
- *
- * @hide
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@RestrictTo(RestrictTo.Scope.TESTS)
-public class TimestampCaptureProcessor implements CaptureProcessor {
- private CaptureProcessor mCaptureProcessor;
- private TimestampListener mTimestampListener;
-
- /**
- * @param captureProcessor The {@link CaptureProcessor} that is wrapped.
- * @param timestampListener The listener which receives the timestamp of all
- * {@link ImageProxy} which are processed.
- */
- public TimestampCaptureProcessor(@NonNull CaptureProcessor captureProcessor,
- @NonNull TimestampListener timestampListener) {
- mCaptureProcessor = captureProcessor;
- mTimestampListener = timestampListener;
- }
-
- /**
- * Interface for receiving the timestamps of all {@link ImageProxy} which are processed by the
- * wrapped {@link CaptureProcessor}.
- */
- @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
- public interface TimestampListener {
- /**
- * Called whenever an {@link ImageProxy} is processed.
- *
- * @param timestamp The timestamp of the {@link ImageProxy} that is processed.
- */
- void onTimestampAvailable(long timestamp);
- }
-
- @Override
- public void onOutputSurface(@NonNull Surface surface, int imageFormat) {
- mCaptureProcessor.onOutputSurface(surface, imageFormat);
- }
-
- @Override
- @ExperimentalGetImage
- public void process(@NonNull ImageProxyBundle bundle) {
- List<Integer> ids = bundle.getCaptureIds();
- Preconditions.checkArgument(ids.size() == 1,
- "Processing preview bundle must be 1, but found " + ids.size());
-
- ListenableFuture<ImageProxy> imageProxyListenableFuture = bundle.getImageProxy(ids.get(0));
- Preconditions.checkArgument(imageProxyListenableFuture.isDone());
-
- try {
- ImageProxy imageProxy = imageProxyListenableFuture.get();
- Image image = imageProxy.getImage();
- if (image == null) {
- return;
- }
-
- // Send timestamp
- mTimestampListener.onTimestampAvailable(image.getTimestamp());
- mCaptureProcessor.process(bundle);
- } catch (ExecutionException | InterruptedException e) {
- // Intentionally empty. Only the ImageProxy which can be retrieved need to have its
- // timestamp captured.
- }
- }
-
- @Override
- public void onResolutionUpdate(@NonNull Size size) {
- mCaptureProcessor.onResolutionUpdate(size);
- }
-}
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-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index b33eb0f..96e9f73 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Requires API 23 for ImageWriter
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
index 4c2e2a1..18c9c9d 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
@@ -53,7 +53,10 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Implement the logic to check whether the extension function is supported or not.
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return false;
+ }
if (cameraCharacteristics == null) {
return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 39e2fb8..4257f2c 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -72,8 +72,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Requires API 23 for ImageWriter
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
index ba8c013..1665c36 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
@@ -59,7 +59,10 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Implement the logic to check whether the extension function is supported or not.
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return false;
+ }
if (cameraCharacteristics == null) {
return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index 0eedc2d..fdb7e01 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Requires API 23 for ImageWriter
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 60d6246..99fb8d9 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -59,7 +59,10 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Implement the logic to check whether the extension function is supported or not.
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return false;
+ }
if (cameraCharacteristics == null) {
return false;
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index a5cdf2a..6a16dff 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -74,8 +74,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Requires API 23 for ImageWriter
- if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
index 09c2ca37..defe8e9 100644
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/HdrPreviewExtenderImpl.java
@@ -20,6 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Pair;
@@ -63,8 +64,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Implement the logic to check whether the extension function is supported or not.
- return true;
+ // Return false to skip tests since old devices do not support extensions.
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@NonNull
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 4febfb4..98c171e 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -67,8 +67,8 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Requires API 23 for ImageWriter
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return false;
}
diff --git a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
index f9b95c0..46512ee 100755
--- a/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
+++ b/camera/camera-testlib-extensions/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
@@ -53,7 +53,10 @@
@Override
public boolean isExtensionAvailable(@NonNull String cameraId,
@Nullable CameraCharacteristics cameraCharacteristics) {
- // Implement the logic to check whether the extension function is supported or not.
+ // Return false to skip tests since old devices do not support extensions.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return false;
+ }
if (cameraCharacteristics == null) {
return false;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 5f8641f..416e857 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
@@ -45,6 +45,7 @@
import static java.util.Collections.singletonList;
import static java.util.Objects.requireNonNull;
+import android.annotation.SuppressLint;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.hardware.camera2.CameraDevice;
@@ -98,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;
@@ -491,6 +491,7 @@
return null;
}
+ @SuppressLint("WrongConstant")
@MainThread
@NonNull
private SessionConfig.Builder createPipeline(@NonNull String cameraId,
@@ -529,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(() -> {
@@ -542,7 +547,7 @@
}
}, CameraXExecutors.mainThreadExecutor());
} else {
- mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange,
+ mSurfaceRequest = new SurfaceRequest(resolution, camera, targetFpsRange,
onSurfaceInvalidated);
mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
// When camera buffers from a REALTIME device are passed directly to a video encoder
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index b618ac7..ff30013 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -202,6 +202,7 @@
Long mLastDataStopTimestamp = null;
@SuppressWarnings("WeakerAccess") // synthetic accessor
Future<?> mStopTimeoutFuture = null;
+ private MediaCodecCallback mMediaCodecCallback = null;
private boolean mIsFlushedAfterEndOfStream = false;
private boolean mSourceStoppedSignalled = false;
@@ -282,7 +283,12 @@
mStopTimeoutFuture.cancel(true);
mStopTimeoutFuture = null;
}
- mMediaCodec.setCallback(new MediaCodecCallback());
+ if (mMediaCodecCallback != null) {
+ mMediaCodecCallback.stop();
+ }
+ mMediaCodecCallback = new MediaCodecCallback();
+ mMediaCodec.setCallback(mMediaCodecCallback);
+
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (mEncoderInput instanceof SurfaceInput) {
@@ -1014,6 +1020,7 @@
private long mLastSentAdjustedTimeUs = 0L;
private boolean mIsOutputBufferInPauseState = false;
private boolean mIsKeyFrameRequired = false;
+ private boolean mStopped = false;
MediaCodecCallback() {
if (mIsVideoEncoder) {
@@ -1032,6 +1039,10 @@
@Override
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives input frame after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1057,6 +1068,10 @@
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
@NonNull BufferInfo bufferInfo) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives frame after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1374,6 +1389,10 @@
public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec,
@NonNull MediaFormat mediaFormat) {
mEncoderExecutor.execute(() -> {
+ if (mStopped) {
+ Logger.w(mTag, "Receives onOutputFormatChanged after codec is reset.");
+ return;
+ }
switch (mState) {
case STARTED:
case PAUSED:
@@ -1404,6 +1423,12 @@
}
});
}
+
+ /** Stop process further frame output. */
+ @ExecutedBy("mEncoderExecutor")
+ void stop() {
+ mStopped = true;
+ }
}
@SuppressWarnings("WeakerAccess") // synthetic accessor
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/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
index 630e7f3..fcd7805 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
@@ -112,6 +112,35 @@
}
}
+ @Test(expected = IllegalArgumentException::class)
+ fun setInvalidEffectsCombination_throwsException() {
+ // Arrange: setup PreviewView and CameraController
+ var previewView: PreviewView? = null
+ activityScenario!!.onActivity {
+ // Arrange.
+ previewView = PreviewView(context)
+ it.setContentView(previewView)
+ previewView!!.controller = controller
+ controller!!.bindToLifecycle(FakeLifecycleOwner())
+ controller!!.initializationFuture.get()
+ }
+ waitUtilPreviewViewIsReady(previewView!!)
+
+ // Act: set the same effect twice, which is invalid.
+ val previewEffect = FakePreviewEffect(
+ mainThreadExecutor(),
+ FakeSurfaceProcessor(mainThreadExecutor())
+ )
+ instrumentation.runOnMainSync {
+ controller!!.setEffects(
+ listOf(
+ previewEffect,
+ previewEffect
+ )
+ )
+ }
+ }
+
@Test
fun setEffectBundle_effectSetOnUseCase() {
// Arrange: setup PreviewView and CameraController
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index 5df9f75..bccd2e9 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -17,7 +17,6 @@
import android.content.Context
import android.graphics.Bitmap
-import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.os.Build
@@ -44,15 +43,12 @@
import androidx.camera.core.SurfaceRequest
import androidx.camera.core.ViewPort
import androidx.camera.core.impl.CameraInfoInternal
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.futures.FutureCallback
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.SurfaceFormatUtil
import androidx.camera.testing.fakes.FakeActivity
import androidx.camera.testing.fakes.FakeCamera
import androidx.camera.testing.fakes.FakeCameraInfoInternal
@@ -578,33 +574,6 @@
}
@Test
- fun correctSurfacePixelFormat_whenRGBA8888IsRequired() {
- val cameraInfo = createCameraInfo(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2)
- val surfaceRequest = createRgb8888SurfaceRequest(cameraInfo)
- val future = surfaceRequest.deferrableSurface.surface
- activityScenario!!.onActivity {
- val previewView = PreviewView(context)
- setContentView(previewView)
- previewView.implementationMode = ImplementationMode.PERFORMANCE
- val surfaceProvider = previewView.surfaceProvider
- surfaceProvider.onSurfaceRequested(surfaceRequest)
- }
- val surface = arrayOfNulls<Surface>(1)
- val countDownLatch = CountDownLatch(1)
- Futures.addCallback(future, object : FutureCallback<Surface?> {
- override fun onSuccess(result: Surface?) {
- surface[0] = result
- countDownLatch.countDown()
- }
-
- override fun onFailure(t: Throwable) {}
- }, CameraXExecutors.directExecutor())
- Truth.assertThat(countDownLatch.await(TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)).isTrue()
- Truth.assertThat(SurfaceFormatUtil.getSurfaceFormat(surface[0]))
- .isEqualTo(PixelFormat.RGBA_8888)
- }
-
- @Test
fun canCreateValidMeteringPoint() {
val cameraInfo = createCameraInfo(
90,
@@ -1060,18 +1029,12 @@
activityScenario!!.onActivity { activity: FakeActivity -> activity.setContentView(view) }
}
- private fun createRgb8888SurfaceRequest(cameraInfo: CameraInfoInternal): SurfaceRequest {
- return createSurfaceRequest(cameraInfo, true)
- }
-
private fun createSurfaceRequest(
cameraInfo: CameraInfoInternal,
- isRGBA8888Required: Boolean = false
): SurfaceRequest {
val fakeCamera = FakeCamera( /*cameraControl=*/null, cameraInfo)
val surfaceRequest = SurfaceRequest(
- DEFAULT_SURFACE_SIZE, fakeCamera,
- isRGBA8888Required
+ DEFAULT_SURFACE_SIZE, fakeCamera
) {}
surfaceRequestList.add(surfaceRequest)
return surfaceRequest
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
index 3e0fede..a3f1971 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
@@ -68,7 +68,7 @@
mParent = FrameLayout(mContext)
setContentView(mParent)
- mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
+ mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera()) {}
mImplementation = SurfaceViewImplementation(mParent, PreviewTransformation())
}
@@ -92,7 +92,7 @@
val previousSurfaceView = mImplementation.mSurfaceView
// Act.
- val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
+ val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera()) {}
mImplementation.testSurfaceRequest(sameResolutionSurfaceRequest)
val newSurfaceView = mImplementation.mSurfaceView
@@ -110,7 +110,7 @@
// Act.
val differentSize: Size by lazy { Size(720, 480) }
val differentResolutionSurfaceRequest =
- SurfaceRequest(differentSize, FakeCamera(), false) {}
+ SurfaceRequest(differentSize, FakeCamera()) {}
mImplementation.testSurfaceRequest(differentResolutionSurfaceRequest)
val newSurfaceView = mImplementation.mSurfaceView
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
index c6998bd..20d9291 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
@@ -50,7 +50,7 @@
private val surfaceRequest: SurfaceRequest
get() {
if (_surfaceRequest == null) {
- _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
+ _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera()) {}
}
return _surfaceRequest!!
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index f92738c..6a9ec13 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -420,6 +420,9 @@
/**
* Implemented by children to refresh after {@link UseCase} is changed.
+ *
+ * @throws IllegalStateException for invalid {@link UseCase} combinations.
+ * @throws RuntimeException for invalid {@link CameraEffect} combinations.
*/
@Nullable
abstract Camera startCamera();
@@ -1164,13 +1167,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
@@ -1204,13 +1208,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
@@ -1242,13 +1247,14 @@
* permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
- * @param audioConfig the configuration of audio.
- * @param executor the executor that the event listener will be run on.
- * @param listener the event listener to handle video record events.
+ * @param audioConfig the configuration of audio.
+ * @param executor the executor that the event listener will be run on.
+ * @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the audio config specifies audio should be enabled but the
- * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission
+ * is denied.
*/
@SuppressLint("MissingPermission")
@ExperimentalVideo
@@ -1925,21 +1931,19 @@
/**
* @param restoreStateRunnable runnable to restore the controller to the previous good state if
* the binding fails.
- * @throws IllegalStateException if binding fails.
+ * @throws IllegalStateException for invalid {@link UseCase} combinations.
+ * @throws RuntimeException for invalid {@link CameraEffect} combinations.
*/
void startCameraAndTrackStates(@Nullable Runnable restoreStateRunnable) {
try {
mCamera = startCamera();
- } catch (IllegalArgumentException exception) {
+ } catch (RuntimeException exception) {
+ // Restore the previous state before re-throwing the exception.
if (restoreStateRunnable != null) {
restoreStateRunnable.run();
}
- // Catches the core exception and throw a more readable one.
- String errorMessage =
- "The selected camera does not support the enabled use cases. Please "
- + "disable use case and/or select a different camera. e.g. "
- + "#setVideoCaptureEnabled(false)";
- throw new IllegalStateException(errorMessage, exception);
+ // This exception will handled by the app.
+ throw exception;
}
if (!isCameraAttached()) {
Logger.d(TAG, CAMERA_NOT_ATTACHED);
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
index 2f751af..9f695f9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
@@ -30,6 +30,7 @@
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.camera.core.Camera;
+import androidx.camera.core.CameraEffect;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.core.impl.utils.Threads;
@@ -109,6 +110,8 @@
* Unbind and rebind all use cases to {@link LifecycleOwner}.
*
* @return null if failed to start camera.
+ * @throws IllegalStateException for invalid {@link UseCase} combinations.
+ * @throws RuntimeException for invalid {@link CameraEffect} combinations.
*/
@RequiresPermission(Manifest.permission.CAMERA)
@Override
@@ -128,7 +131,16 @@
// Use cases can't be created.
return null;
}
- return mCameraProvider.bindToLifecycle(mLifecycleOwner, mCameraSelector, useCaseGroup);
+ try {
+ return mCameraProvider.bindToLifecycle(mLifecycleOwner, mCameraSelector, useCaseGroup);
+ } catch (IllegalArgumentException e) {
+ // Catches the invalid use case combination exception and throw a more readable one.
+ String errorMessage =
+ "The selected camera does not support the enabled use cases. Please "
+ + "disable use case and/or select a different camera. e.g. "
+ + "#setVideoCaptureEnabled(false)";
+ throw new IllegalStateException(errorMessage, e);
+ }
}
/**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index 1d141c6..1f3921e 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -681,10 +681,9 @@
.getImplementationType().equals(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
|| DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
- if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice
- || hasSurfaceViewQuirk) {
+ if (Build.VERSION.SDK_INT <= 24 || isLegacyDevice || hasSurfaceViewQuirk) {
// Force to use TextureView when the device is running android 7.0 and below, legacy
- // level, RGBA8888 is required or SurfaceView has quirks.
+ // level or SurfaceView has quirks.
return true;
}
switch (implementationMode) {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java b/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
index 9c441dc..6994283 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/transform/CoordinateTransform.java
@@ -83,9 +83,8 @@
// the transform from sensor to surface. But it will require the view artifact to
// depend on a new internal API in the core artifact, which we can't do at the
// moment because of the version mismatch between view and core.
- if (!isAspectRatioMatchingWithRoundingError(
- source.getViewPortSize(), /* isAccurate1= */ false,
- target.getViewPortSize(), /* isAccurate2= */ false)) {
+ if (!isAspectRatioMatchingWithRoundingError(source.getViewPortSize(),
+ target.getViewPortSize())) {
// Mismatched aspect ratio means the outputs are not associated with the same Viewport.
Logger.w(TAG, String.format(MISMATCH_MSG, source.getViewPortSize(),
target.getViewPortSize()));
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
index 804de8a..fb1c45d 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
@@ -139,7 +139,6 @@
FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
return new SurfaceRequest(new Size(800, 600),
- new FakeCamera(null, cameraInfoInternal),
- /*isRGB8888Required*/ false, () -> {});
+ new FakeCamera(null, cameraInfoInternal), () -> {});
}
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 1781ca6..d13c4c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -61,16 +61,29 @@
/**
* Waits until an image has been saved and its idling resource has become idle.
+ *
+ * @param captureRequestsCount the capture requests count to issue to continuously take pictures
+ * without waiting for the previous capture requests to be done.
*/
-internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
+internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle(
+ captureRequestsCount: Int = 1
+) {
val idlingResource = withActivity {
cleanTakePictureErrorMessage()
imageSavedIdlingResource
}
try {
- IdlingRegistry.getInstance().register(idlingResource)
// Perform click to take a picture.
- Espresso.onView(ViewMatchers.withId(R.id.Picture)).perform(click())
+ Espresso.onView(ViewMatchers.withId(R.id.Picture)).apply {
+ repeat(captureRequestsCount) {
+ perform(click())
+ }
+ }
+ // Registers the idling resource and wait for it being idle after performing the click
+ // operations. So that the click operations can be performed continuously without wait for
+ // previous capture results.
+ IdlingRegistry.getInstance().register(idlingResource)
+ Espresso.onIdle()
} finally { // Always release the idling resource, in case of timeout exceptions.
IdlingRegistry.getInstance().unregister(idlingResource)
withActivity {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 1d83c85..e812cab 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -1197,10 +1197,7 @@
assertThat(imageCapture.flashMode).isEqualTo(ImageCapture.FLASH_MODE_ON)
}
- // Output JPEG format image when setting a CaptureProcessor is only enabled for devices whose
- // API level is at least 29.
@Test
- @SdkSuppress(minSdkVersion = 29)
fun returnJpegImage_whenSoftwareJpegIsEnabled() = runBlocking {
val builder = ImageCapture.Builder()
@@ -1234,7 +1231,61 @@
}
@Test
- @SdkSuppress(minSdkVersion = 26)
+ fun canSaveJpegFileWithRotation_whenSoftwareJpegIsEnabled() = runBlocking {
+ val builder = ImageCapture.Builder()
+
+ // Enables software Jpeg
+ builder.mutableConfig.insertOption(
+ ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER,
+ true
+ )
+ val useCase = builder.build()
+ var camera: Camera
+ withContext(Dispatchers.Main) {
+ camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+ }
+
+ val saveLocation = File.createTempFile("test", ".jpg")
+ saveLocation.deleteOnExit()
+ val callback = FakeImageSavedCallback(capturesCount = 1)
+ useCase.takePicture(
+ ImageCapture.OutputFileOptions.Builder(saveLocation).build(),
+ mainExecutor, callback)
+
+ // Wait for the signal that the image has been captured and saved.
+ callback.awaitCapturesAndAssert(savedImagesCount = 1)
+
+ // For YUV to JPEG case, the rotation will only be in Exif.
+ val exif = Exif.createFromFile(saveLocation)
+ assertThat(exif.rotation).isEqualTo(
+ camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation))
+ }
+
+ @Test
+ fun returnYuvImage_withYuvBufferFormat() = runBlocking {
+ val builder = ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888)
+ val useCase = builder.build()
+ var camera: Camera
+ withContext(Dispatchers.Main) {
+ camera = cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+ }
+
+ val callback = FakeImageCaptureCallback(capturesCount = 1)
+ useCase.takePicture(mainExecutor, callback)
+
+ // Wait for the signal that the image has been captured.
+ callback.awaitCapturesAndAssert(capturedImagesCount = 1)
+
+ val imageProperties = callback.results.first()
+ // Check the output image rotation degrees value is correct.
+ assertThat(imageProperties.rotationDegrees).isEqualTo(
+ camera.cameraInfo.getSensorRotationDegrees(useCase.targetRotation)
+ )
+ // Check the output format is correct.
+ assertThat(imageProperties.format).isEqualTo(ImageFormat.YUV_420_888)
+ }
+
+ @Test
fun returnYuvImage_whenSoftwareJpegIsEnabledWithYuvBufferFormat() = runBlocking {
val builder = ImageCapture.Builder().setBufferFormat(ImageFormat.YUV_420_888)
@@ -1268,7 +1319,7 @@
@Test
@SdkSuppress(minSdkVersion = 28)
- fun returnJpegImage_whenSessionProcessorIsSet_outputFormantYuv() = runBlocking {
+ fun returnJpegImage_whenSessionProcessorIsSet() = runBlocking {
val builder = ImageCapture.Builder()
val sessionProcessor = FakeSessionProcessor(
inputFormatPreview = null, // null means using the same output surface
@@ -1306,7 +1357,7 @@
@Test
@SdkSuppress(minSdkVersion = 28)
- fun returnJpegImage_whenSessionProcessorIsSet_outputFormantJpeg() = runBlocking {
+ fun returnJpegImage_whenSessionProcessorIsSet_outputFormatJpeg() = runBlocking {
assumeFalse(
"Cuttlefish does not correctly handle Jpeg exif. Unable to test.",
Build.MODEL.contains("Cuttlefish")
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index a447c7f..578ae88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -273,4 +273,24 @@
val randomDelayDuration = (Random.nextInt(maxDelaySeconds) + 1).toLong()
delay(TimeUnit.SECONDS.toMillis(randomDelayDuration))
}
+
+ @LabTestRule.LabTestOnly
+ @Test
+ @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
+ fun launchActivity_thenTakeMultiplePictures_withoutWaitingPreviousResults() {
+ val useCaseCombination = BIND_PREVIEW or BIND_IMAGE_CAPTURE
+
+ // Launches CameraXActivity and wait for the preview ready.
+ val activityScenario =
+ launchCameraXActivityAndWaitForPreviewReady(cameraId, useCaseCombination)
+
+ with(activityScenario) {
+ use {
+ // Checks whether multiple images can be captured successfully
+ repeat(STRESS_TEST_OPERATION_REPEAT_COUNT) {
+ takePictureAndWaitForImageSavedIdle(3)
+ }
+ }
+ }
+ }
}
\ No newline at end of file
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/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index a01aa33..de01a82 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -366,6 +366,7 @@
@androidx.compose.foundation.ExperimentalFoundationApi public final class SnapFlingBehavior implements androidx.compose.foundation.gestures.FlingBehavior {
ctor public SnapFlingBehavior(androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider snapLayoutInfoProvider, androidx.compose.animation.core.AnimationSpec<java.lang.Float> lowVelocityAnimationSpec, androidx.compose.animation.core.DecayAnimationSpec<java.lang.Float> highVelocityAnimationSpec, androidx.compose.animation.core.AnimationSpec<java.lang.Float> snapAnimationSpec, androidx.compose.ui.unit.Density density, optional float shortSnapVelocityThreshold);
method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.coroutines.Continuation<? super java.lang.Float>);
+ method public suspend Object? performFling(androidx.compose.foundation.gestures.ScrollScope, float initialVelocity, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onSettlingDistanceUpdated, kotlin.coroutines.Continuation<? super java.lang.Float>);
}
public final class SnapFlingBehaviorKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
index 2428fea..d43a2b8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapFlingBehaviorTest.kt
@@ -17,8 +17,11 @@
package androidx.compose.foundation.gesture.snapping
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.MinFlingVelocityDp
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
@@ -46,6 +49,7 @@
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.test.assertEquals
@@ -64,6 +68,9 @@
private val density: Density
get() = rule.density
+ private lateinit var snapLayoutInfoProvider: SnapLayoutInfoProvider
+ private lateinit var snapFlingBehavior: FlingBehavior
+
@Test
fun belowThresholdVelocity_lessThanAnItemScroll_shouldStayInSamePage() {
var lazyListState: LazyListState? = null
@@ -360,15 +367,73 @@
}
}
+ @Test
+ fun remainingScrollOffset_shouldFollowAnimationOffsets() {
+ var stepSize = 0f
+ var velocityThreshold = 0f
+ val scrollOffset = mutableListOf<Float>()
+ // arrange
+ rule.setContent {
+ val density = LocalDensity.current
+ val state = rememberLazyListState()
+ stepSize = with(density) { ItemSize.toPx() }
+ velocityThreshold = with(density) { MinFlingVelocityDp.toPx() }
+ MainLayout(state = state, scrollOffset)
+ }
+
+ rule.mainClock.autoAdvance = false
+ // act
+ val velocity = velocityThreshold * 3
+ onMainList().performTouchInput {
+ swipeMainAxisWithVelocity(
+ 1.5f * stepSize,
+ velocity
+ )
+ }
+ rule.mainClock.advanceTimeByFrame()
+
+ // assert
+ val initialTargetOffset =
+ with(snapLayoutInfoProvider) { density.calculateApproachOffset(velocity) }
+ Truth.assertThat(scrollOffset.first { it != 0f }).isWithin(0.5f)
+ .of(initialTargetOffset)
+
+ // act: wait for remaining offset to grow instead of decay, this indicates the last
+ // snap step will start
+ rule.mainClock.advanceTimeUntil {
+ scrollOffset.size > 2 &&
+ scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+ }
+
+ // assert: next calculated bound is the first value emitted by remainingScrollOffset
+ val bounds = with(snapLayoutInfoProvider) { density.calculateSnappingOffsetBounds() }
+ val finalRemainingOffset = bounds.endInclusive
+ Truth.assertThat(scrollOffset.last()).isWithin(0.5f)
+ .of(finalRemainingOffset)
+ rule.mainClock.autoAdvance = true
+
+ // assert: value settles back to zero
+ rule.runOnIdle {
+ Truth.assertThat(scrollOffset.last()).isEqualTo(0f)
+ }
+ }
+
private fun onMainList() = rule.onNodeWithTag(TestTag)
@Composable
- fun MainLayout(state: LazyListState) {
- val layoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+ fun MainLayout(state: LazyListState, scrollOffset: MutableList<Float> = mutableListOf()) {
+ snapLayoutInfoProvider = remember(state) { SnapLayoutInfoProvider(state) }
+ val innerFlingBehavior =
+ rememberSnapFlingBehavior(snapLayoutInfoProvider = snapLayoutInfoProvider)
+ snapFlingBehavior = remember(innerFlingBehavior) {
+ QuerySnapFlingBehavior(innerFlingBehavior) {
+ scrollOffset.add(it)
+ }
+ }
LazyColumnOrRow(
state = state,
modifier = Modifier.testTag(TestTag),
- flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider = layoutInfoProvider)
+ flingBehavior = snapFlingBehavior
) {
items(200) {
Box(modifier = Modifier.size(ItemSize))
@@ -435,4 +500,16 @@
val CenterToCenter: Density.(Float, Float) -> Float =
{ layoutSize, itemSize -> layoutSize / 2f - itemSize / 2f }
}
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private class QuerySnapFlingBehavior(
+ val snapFlingBehavior: SnapFlingBehavior,
+ val onAnimationStep: (Float) -> Unit
+) : FlingBehavior {
+ override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+ return with(snapFlingBehavior) {
+ performFling(initialVelocity, onAnimationStep)
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index a09f72b..543f121 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -64,10 +64,10 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth
import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import kotlin.test.assertNotEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -112,6 +112,86 @@
}
@Test
+ fun remainingScrollOffset_whenVelocityIsBelowThreshold_shouldRepresentShortSnapOffsets() {
+ val testLayoutInfoProvider = TestLayoutInfoProvider()
+ lateinit var testFlingBehavior: SnapFlingBehavior
+ val scrollOffset = mutableListOf<Float>()
+ rule.setContent {
+ testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+ VelocityEffect(
+ testFlingBehavior,
+ calculateVelocityThreshold() - 1
+ ) { remainingScrollOffset ->
+ scrollOffset.add(remainingScrollOffset)
+ }
+ }
+
+ // Will Snap Back
+ rule.runOnIdle {
+ assertEquals(scrollOffset.first(), testLayoutInfoProvider.minOffset)
+ assertEquals(scrollOffset.last(), 0f)
+ }
+ }
+
+ @Test
+ fun remainingScrollOffset_whenVelocityIsAboveThreshold_shouldRepresentLongSnapOffsets() {
+ val testLayoutInfoProvider = TestLayoutInfoProvider()
+ lateinit var testFlingBehavior: SnapFlingBehavior
+ val scrollOffset = mutableListOf<Float>()
+ rule.setContent {
+ testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+ VelocityEffect(
+ testFlingBehavior,
+ calculateVelocityThreshold() + 1
+ ) { remainingScrollOffset ->
+ scrollOffset.add(remainingScrollOffset)
+ }
+ }
+
+ rule.runOnIdle {
+ assertEquals(scrollOffset.first { it != 0f }, testLayoutInfoProvider.maxOffset)
+ assertEquals(scrollOffset.last(), 0f)
+ }
+ }
+
+ @Test
+ fun remainingScrollOffset_longSnap_targetShouldChangeInAccordanceWithAnimation() {
+ // Arrange
+ val initialOffset = 250f
+ val testLayoutInfoProvider = TestLayoutInfoProvider(approachOffset = initialOffset)
+ lateinit var testFlingBehavior: SnapFlingBehavior
+ val scrollOffset = mutableListOf<Float>()
+ rule.mainClock.autoAdvance = false
+ rule.setContent {
+ testFlingBehavior = rememberSnapFlingBehavior(testLayoutInfoProvider)
+ VelocityEffect(
+ testFlingBehavior,
+ calculateVelocityThreshold() + 1
+ ) { remainingScrollOffset ->
+ scrollOffset.add(remainingScrollOffset)
+ }
+ }
+
+ // assert the initial value emitted by remainingScrollOffset was the one provider by the
+ // snap layout info provider
+ assertEquals(scrollOffset.first(), initialOffset)
+
+ // Act: Advance until remainingScrollOffset grows again
+ rule.mainClock.advanceTimeUntil {
+ scrollOffset.size > 2 &&
+ scrollOffset.last() > scrollOffset[scrollOffset.lastIndex - 1]
+ }
+
+ assertEquals(scrollOffset.last(), testLayoutInfoProvider.maxOffset)
+
+ rule.mainClock.autoAdvance = true
+ // Assert
+ rule.runOnIdle {
+ assertEquals(scrollOffset.last(), 0f)
+ }
+ }
+
+ @Test
fun performFling_afterSnappingVelocity_everythingWasConsumed_shouldReturnNoVelocity() {
val testLayoutInfoProvider = TestLayoutInfoProvider()
var afterFlingVelocity = 0f
@@ -360,13 +440,18 @@
}
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun VelocityEffect(testFlingBehavior: FlingBehavior, velocity: Float) {
+private fun VelocityEffect(
+ testFlingBehavior: FlingBehavior,
+ velocity: Float,
+ onSettlingDistanceUpdated: (Float) -> Unit = {}
+) {
val scrollableState = rememberScrollableState(consumeScrollDelta = { it })
LaunchedEffect(Unit) {
scrollableState.scroll {
- with(testFlingBehavior) {
- performFling(velocity)
+ with(testFlingBehavior as SnapFlingBehavior) {
+ performFling(velocity, onSettlingDistanceUpdated)
}
}
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
index cfd4977..f1b9c98 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
@@ -21,13 +21,14 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -319,9 +320,24 @@
fun scrollingBackReusesTheSameSlot() {
lateinit var state: LazyGridState
var counter0 = 0
- var counter1 = 10
- var rememberedValue0 = -1
- var rememberedValue1 = -1
+ var counter1 = 0
+
+ val measureCountModifier0 = Modifier.layout { measurable, constraints ->
+ counter0++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
+ val measureCountModifier1 = Modifier.layout { measurable, constraints ->
+ counter1++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
rule.setContent {
state = rememberLazyGridState()
LazyVerticalGrid(
@@ -330,13 +346,17 @@
state
) {
items(100) {
- if (it == 0) {
- rememberedValue0 = remember { counter0++ }
+ val modifier = when (it) {
+ 0 -> measureCountModifier0
+ 1 -> measureCountModifier1
+ else -> Modifier
}
- if (it == 1) {
- rememberedValue1 = remember { counter1++ }
- }
- Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ Spacer(
+ Modifier
+ .height(itemsSizeDp)
+ .testTag("$it")
+ .then(modifier)
+ )
}
}
}
@@ -348,10 +368,10 @@
}
rule.runOnIdle {
- Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
- .that(rememberedValue0).isEqualTo(0)
- Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
- .that(rememberedValue1).isEqualTo(10)
+ Truth.assertWithMessage("Item 0 measured $counter0 times, expected 1.")
+ .that(counter0).isEqualTo(1)
+ Truth.assertWithMessage("Item 1 measured $counter1 times, expected 1.")
+ .that(counter1).isEqualTo(1)
}
rule.onNodeWithTag("0")
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/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index d3e9d7a..836604f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -19,6 +19,7 @@
import android.os.Parcelable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -30,6 +31,8 @@
import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.Remeasurement
+import androidx.compose.ui.layout.RemeasurementModifier
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
@@ -94,7 +97,10 @@
@Test
fun measureAndPlaceTwoItems() {
val itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
LazyLayout(itemProvider) {
@@ -118,8 +124,14 @@
@Test
fun measureAndPlaceMultipleLayoutsInOneItem() {
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("${index}x0"))
- Box(Modifier.fillMaxSize().testTag("${index}x1"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x0"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("${index}x1"))
}
rule.setContent {
@@ -143,7 +155,10 @@
@Test
fun updatingitemProvider() {
var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
})
rule.setContent {
@@ -166,7 +181,10 @@
rule.runOnIdle {
itemProvider = itemProvider({ 2 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
}
@@ -178,7 +196,10 @@
fun stateBaseditemProvider() {
var itemCount by mutableStateOf(1)
val itemProvider = itemProvider({ itemCount }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index"))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index"))
}
rule.setContent {
@@ -228,7 +249,11 @@
}
}
val itemProvider = itemProvider({ 1 }) { index ->
- Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
+ Box(
+ Modifier
+ .fillMaxSize()
+ .testTag("$index")
+ .then(modifier))
}
var needToCompose by mutableStateOf(false)
val prefetchState = LazyLayoutPrefetchState()
@@ -335,13 +360,15 @@
fun nodeIsReusedWithoutExtraRemeasure() {
var indexToCompose by mutableStateOf<Int?>(0)
var remeasuresCount = 0
- val modifier = Modifier.layout { measurable, constraints ->
- val placeable = measurable.measure(constraints)
- remeasuresCount++
- layout(placeable.width, placeable.height) {
- placeable.place(0, 0)
+ val modifier = Modifier
+ .layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ remeasuresCount++
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
}
- }.fillMaxSize()
+ .fillMaxSize()
val itemProvider = itemProvider({ 2 }) {
Box(modifier)
}
@@ -375,6 +402,55 @@
}
}
+ @Test
+ fun subcomposeNodeContentIsResetWhenReused() {
+ var indexToCompose by mutableStateOf(0)
+ var remeasurement: Remeasurement? = null
+ val itemProvider = itemProvider({ 3 }) {
+ BoxWithConstraints(
+ Modifier.testTag("Box $it")
+ ) {
+ Box(Modifier.testTag("$it"))
+ }
+ }
+
+ rule.setContent {
+ LazyLayout(
+ itemProvider = itemProvider,
+ modifier = object : RemeasurementModifier {
+ @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
+ override fun onRemeasurementAvailable(value: Remeasurement) {
+ remeasurement = value
+ }
+ }
+ ) { constraints ->
+ val node = measure(indexToCompose, constraints).first()
+ layout(node.width, node.height) {
+ node.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ indexToCompose = 1
+ remeasurement?.forceRemeasure()
+ indexToCompose = 2
+ remeasurement?.forceRemeasure()
+ }
+
+ rule.onNodeWithTag("Box 0")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("0")
+ .assertDoesNotExist()
+
+ rule.onNodeWithTag("Box 2")
+ .assertExists()
+
+ rule.onNodeWithTag("2")
+ .assertExists()
+ }
+
private fun itemProvider(
itemCount: () -> Int,
itemContent: @Composable (Int) -> Unit
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
index 967acf3..2856359 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
@@ -48,6 +48,7 @@
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
@@ -503,6 +504,49 @@
}
}
+ @Test
+ fun nestedLazyRowChildrenAreReused() {
+ lateinit var state: LazyListState
+ var remeasuresCount = 0
+ val measureModifier = Modifier.layout { _, constraints ->
+ remeasuresCount++
+ layout(constraints.maxWidth, constraints.maxHeight) {}
+ }
+ rule.setContentWithTestViewConfiguration {
+ state = rememberLazyListState()
+ LazyColumn(
+ Modifier
+ .fillMaxWidth()
+ .height(10.dp),
+ state = state
+ ) {
+ items(100) {
+ LazyRow {
+ item {
+ Box(Modifier.size(25.dp).then(measureModifier))
+ }
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ state.prefetchingEnabled = false
+ runBlocking {
+ state.scrollToItem(1) // now item 0 should be kept for reuse
+ assertThat(remeasuresCount).isEqualTo(2)
+ remeasuresCount = 0
+ state.scrollToItem(2) // item 2 should reuse item 0 slot
+ }
+ }
+
+ rule.runOnIdle {
+ // no remeasures are expected as the LayoutNode should be reused and modifiers
+ // didn't change.
+ assertThat(remeasuresCount).isEqualTo(0)
+ }
+ }
+
@Composable
private fun LazyRowWrapped(content: @Composable () -> Unit) {
LazyRow {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
index 867fd32..97c63ce 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListSlotsReuseTest.kt
@@ -25,13 +25,14 @@
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -316,9 +317,24 @@
fun scrollingBackReusesTheSameSlot() {
lateinit var state: LazyListState
var counter0 = 0
- var counter1 = 10
- var rememberedValue0 = -1
- var rememberedValue1 = -1
+ var counter1 = 0
+
+ val measureCountModifier0 = Modifier.layout { measurable, constraints ->
+ counter0++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
+ val measureCountModifier1 = Modifier.layout { measurable, constraints ->
+ counter1++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
rule.setContent {
state = rememberLazyListState()
LazyColumn(
@@ -326,13 +342,18 @@
state
) {
items(100) {
- if (it == 0) {
- rememberedValue0 = remember { counter0++ }
+ val modifier = when (it) {
+ 0 -> measureCountModifier0
+ 1 -> measureCountModifier1
+ else -> Modifier
}
- if (it == 1) {
- rememberedValue1 = remember { counter1++ }
- }
- Spacer(Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it"))
+ Spacer(
+ Modifier
+ .height(itemsSizeDp)
+ .fillParentMaxWidth()
+ .testTag("$it")
+ .then(modifier)
+ )
}
}
}
@@ -344,10 +365,10 @@
}
rule.runOnIdle {
- Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
- .that(rememberedValue0).isEqualTo(0)
- Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
- .that(rememberedValue1).isEqualTo(10)
+ Truth.assertWithMessage("Item 0 measured $counter0 times, expected 1.")
+ .that(counter0).isEqualTo(1)
+ Truth.assertWithMessage("Item 1 measured $counter1 times, expected 1.")
+ .that(counter1).isEqualTo(1)
}
rule.onNodeWithTag("0")
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/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index 6e34166..5e7a819 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.textfield
import android.view.KeyEvent
+import android.view.KeyEvent.META_ALT_ON
import android.view.KeyEvent.META_CTRL_ON
import android.view.KeyEvent.META_SHIFT_ON
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -241,6 +242,45 @@
}
@Test
+ fun textField_deleteToBeginningOfLine() {
+ keysSequenceTest(initText = "hello world\nhi world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.Backspace.downAndUp(META_ALT_ON)
+ expectedText(" world\nhi world")
+ Key.Backspace.downAndUp(META_ALT_ON)
+ expectedText(" world\nhi world")
+ repeat(3) { Key.DirectionRight.downAndUp() }
+ Key.Backspace.downAndUp(META_ALT_ON)
+ expectedText("rld\nhi world")
+ Key.DirectionDown.downAndUp()
+ Key.MoveEnd.downAndUp()
+ Key.Backspace.downAndUp(META_ALT_ON)
+ expectedText("rld\n")
+ Key.Backspace.downAndUp(META_ALT_ON)
+ expectedText("rld\n")
+ }
+ }
+
+ @Test
+ fun textField_deleteToEndOfLine() {
+ keysSequenceTest(initText = "hello world\nhi world") {
+ Key.DirectionRight.downAndUp(META_CTRL_ON)
+ Key.Delete.downAndUp(META_ALT_ON)
+ expectedText("hello\nhi world")
+ Key.Delete.downAndUp(META_ALT_ON)
+ expectedText("hello\nhi world")
+ repeat(3) { Key.DirectionRight.downAndUp() }
+ Key.Delete.downAndUp(META_ALT_ON)
+ expectedText("hello\nhi")
+ Key.MoveHome.downAndUp()
+ Key.Delete.downAndUp(META_ALT_ON)
+ expectedText("hello\n")
+ Key.Delete.downAndUp(META_ALT_ON)
+ expectedText("hello\n")
+ }
+ }
+
+ @Test
fun textField_paragraphNavigation() {
keysSequenceTest(initText = "hello world\nhi") {
Key.DirectionDown.downAndUp(META_CTRL_ON)
@@ -352,7 +392,7 @@
sequence: SequenceScope.() -> Unit,
) {
val inputService = TextInputService(mock())
- val focusFequester = FocusRequester()
+ val focusRequester = FocusRequester()
rule.setContent {
CompositionLocalProvider(
LocalTextInputService provides inputService
@@ -363,13 +403,13 @@
fontFamily = TEST_FONT_FAMILY,
fontSize = 10.sp
),
- modifier = modifier.focusRequester(focusFequester),
+ modifier = modifier.focusRequester(focusRequester),
onValueChange = onValueChange
)
}
}
- rule.runOnIdle { focusFequester.requestFocus() }
+ rule.runOnIdle { focusRequester.requestFocus() }
sequence(SequenceScope(value) { rule.onNode(hasSetTextAction()) })
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 59b76f4..b6e53ad 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -94,14 +94,31 @@
internal var motionScaleDuration = DefaultScrollMotionDurationScale
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
- // If snapping from scroll (short snap) or fling (long snap)
+ return performFling(initialVelocity) {}
+ }
+
+ /**
+ * Perform a snapping fling animation with given velocity and suspend until fling has
+ * finished. This will behave the same way as [performFling] except it will report on
+ * each remainingOffsetUpdate using the [onSettlingDistanceUpdated] lambda.
+ *
+ * @param initialVelocity velocity available for fling in the orientation specified in
+ * [androidx.compose.foundation.gestures.scrollable] that invoked this method.
+ *
+ * @param onSettlingDistanceUpdated a lambda that will be called anytime the
+ * distance to the settling offset is updated. The settling offset is the final offset where
+ * this fling will stop and may change depending on the snapping animation progression.
+ *
+ * @return remaining velocity after fling operation has ended
+ */
+ suspend fun ScrollScope.performFling(
+ initialVelocity: Float,
+ onSettlingDistanceUpdated: (Float) -> Unit
+ ): Float {
val (remainingOffset, remainingState) = withContext(motionScaleDuration) {
- if (abs(initialVelocity) <= abs(velocityThreshold)) {
- shortSnap(initialVelocity)
- } else {
- longSnap(initialVelocity)
- }
+ fling(initialVelocity, onSettlingDistanceUpdated)
}
+
debugLog { "Post Settling Offset=$remainingOffset" }
// No remaining offset means we've used everything, no need to propagate velocity. Otherwise
// we couldn't use everything (probably because we have hit the min/max bounds of the
@@ -109,39 +126,83 @@
return if (remainingOffset == 0f) NoVelocity else remainingState.velocity
}
+ private suspend fun ScrollScope.fling(
+ initialVelocity: Float,
+ onRemainingScrollOffsetUpdate: (Float) -> Unit
+ ): AnimationResult<Float, AnimationVector1D> {
+ // If snapping from scroll (short snap) or fling (long snap)
+ val result = withContext(motionScaleDuration) {
+ if (abs(initialVelocity) <= abs(velocityThreshold)) {
+ shortSnap(initialVelocity, onRemainingScrollOffsetUpdate)
+ } else {
+ longSnap(initialVelocity, onRemainingScrollOffsetUpdate)
+ }
+ }
+
+ onRemainingScrollOffsetUpdate(0f) // Animation finished or was cancelled
+ return result
+ }
+
private suspend fun ScrollScope.shortSnap(
- velocity: Float
+ velocity: Float,
+ onRemainingScrollOffsetUpdate: (Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
debugLog { "Short Snapping" }
val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
+ var remainingScrollOffset = closestOffset
+
val animationState = AnimationState(NoDistance, velocity)
- return animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
+ return animateSnap(
+ closestOffset,
+ closestOffset,
+ animationState,
+ snapAnimationSpec
+ ) { delta ->
+ remainingScrollOffset -= delta
+ onRemainingScrollOffsetUpdate(remainingScrollOffset)
+ }
}
private suspend fun ScrollScope.longSnap(
- initialVelocity: Float
+ initialVelocity: Float,
+ onAnimationStep: (remainingScrollOffset: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
debugLog { "Long Snapping" }
val initialOffset =
with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
abs(it) * sign(initialVelocity) // ensure offset sign is correct
}
+ var remainingScrollOffset = initialOffset
- val (remainingOffset, animationState) = runApproach(initialOffset, initialVelocity)
+ onAnimationStep(remainingScrollOffset) // First Scroll Offset
+
+ val (remainingOffset, animationState) = runApproach(
+ initialOffset,
+ initialVelocity
+ ) { delta ->
+ remainingScrollOffset -= delta
+ onAnimationStep(remainingScrollOffset)
+ }
debugLog { "Settling Final Bound=$remainingOffset" }
+ remainingScrollOffset = remainingOffset
+
return animateSnap(
remainingOffset,
remainingOffset,
animationState.copy(value = 0f),
snapAnimationSpec
- )
+ ) { delta ->
+ remainingScrollOffset -= delta
+ onAnimationStep(remainingScrollOffset)
+ }
}
private suspend fun ScrollScope.runApproach(
initialTargetOffset: Float,
- initialVelocity: Float
+ initialVelocity: Float,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
val animation =
@@ -162,7 +223,8 @@
initialVelocity,
animation,
snapLayoutInfoProvider,
- density
+ density,
+ onAnimationStep
)
}
@@ -244,13 +306,15 @@
initialVelocity: Float,
animation: ApproachAnimation<Float, AnimationVector1D>,
snapLayoutInfoProvider: SnapLayoutInfoProvider,
- density: Density
+ density: Density,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
val (_, currentAnimationState) = animation.approachAnimation(
this,
initialTargetOffset,
- initialVelocity
+ initialVelocity,
+ onAnimationStep
)
val remainingOffset =
@@ -318,16 +382,24 @@
/**
* Run a [DecayAnimationSpec] animation up to before [targetOffset] using [animationState]
+ *
+ * @param targetOffset The destination of this animation. Since this is a decay animation, we can
+ * use this value to prevent the animation to run until the end.
+ * @param animationState The previous [AnimationState] for continuation purposes.
+ * @param decayAnimationSpec The [DecayAnimationSpec] that will drive this animation
+ * @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
*/
private suspend fun ScrollScope.animateDecay(
targetOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
- decayAnimationSpec: DecayAnimationSpec<Float>
+ decayAnimationSpec: DecayAnimationSpec<Float>,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
var previousValue = 0f
fun AnimationScope<Float, AnimationVector1D>.consumeDelta(delta: Float) {
val consumed = scrollBy(delta)
+ onAnimationStep(consumed)
if (abs(delta - consumed) > 0.5f) cancelAnimation()
}
@@ -355,12 +427,19 @@
/**
* Runs a [AnimationSpec] to snap the list into [targetOffset]. Uses [cancelOffset] to stop this
* animation before it reaches the target.
+ *
+ * @param targetOffset The final target of this animation
+ * @param cancelOffset If we'd like to finish the animation earlier we use this value
+ * @param animationState The current animation state for continuation purposes
+ * @param snapAnimationSpec The [AnimationSpec] that will drive this animation
+ * @param onAnimationStep Called for each new scroll delta emitted by the animation cycle.
*/
private suspend fun ScrollScope.animateSnap(
targetOffset: Float,
cancelOffset: Float,
animationState: AnimationState<Float, AnimationVector1D>,
- snapAnimationSpec: AnimationSpec<Float>
+ snapAnimationSpec: AnimationSpec<Float>,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
var consumedUpToNow = 0f
val initialVelocity = animationState.velocity
@@ -372,6 +451,7 @@
val realValue = value.coerceToTarget(cancelOffset)
val delta = realValue - consumedUpToNow
val consumed = scrollBy(delta)
+ onAnimationStep(consumed)
// stop when unconsumed or when we reach the desired value
if (abs(delta - consumed) > 0.5f || realValue != value) {
cancelAnimation()
@@ -399,7 +479,8 @@
suspend fun approachAnimation(
scope: ScrollScope,
offset: T,
- velocity: T
+ velocity: T,
+ onAnimationStep: (delta: T) -> Unit
): AnimationResult<T, V>
}
@@ -412,7 +493,8 @@
override suspend fun approachAnimation(
scope: ScrollScope,
offset: Float,
- velocity: Float
+ velocity: Float,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
val targetOffset =
@@ -424,7 +506,8 @@
targetOffset = targetOffset,
cancelOffset = offset,
animationState = animationState,
- snapAnimationSpec = lowVelocityAnimationSpec
+ snapAnimationSpec = lowVelocityAnimationSpec,
+ onAnimationStep = onAnimationStep
)
}
}
@@ -436,11 +519,12 @@
override suspend fun approachAnimation(
scope: ScrollScope,
offset: Float,
- velocity: Float
+ velocity: Float,
+ onAnimationStep: (delta: Float) -> Unit
): AnimationResult<Float, AnimationVector1D> {
val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
return with(scope) {
- animateDecay(offset, animationState, decayAnimationSpec)
+ animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep)
}
}
}
@@ -448,8 +532,8 @@
internal val MinFlingVelocityDp = 400.dp
internal const val NoDistance = 0f
internal const val NoVelocity = 0f
-private const val DEBUG = false
+private const val DEBUG = false
private inline fun debugLog(generateMsg: () -> String) {
if (DEBUG) {
println("SnapFlingBehavior: ${generateMsg()}")
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 6169203..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
@@ -89,7 +90,7 @@
beyondBoundsPageCount: Int = 0,
pageSpacing: Dp = 0.dp,
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+ flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
userScrollEnabled: Boolean = true,
reverseLayout: Boolean = false,
pageContent: @Composable (page: Int) -> Unit
@@ -149,7 +150,7 @@
beyondBoundsPageCount: Int = 0,
pageSpacing: Dp = 0.dp,
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
- flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+ flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state),
userScrollEnabled: Boolean = true,
reverseLayout: Boolean = false,
pageContent: @Composable (page: Int) -> Unit
@@ -184,7 +185,7 @@
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
contentPadding: PaddingValues,
- flingBehavior: FlingBehavior,
+ flingBehavior: SnapFlingBehavior,
userScrollEnabled: Boolean,
reverseLayout: Boolean,
pageContent: @Composable (page: Int) -> Unit
@@ -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,
@@ -365,7 +370,7 @@
lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
- ): FlingBehavior {
+ ): SnapFlingBehavior {
val density = LocalDensity.current
return remember(
@@ -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/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
index f5404bb..c5be867 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyMapping.kt
@@ -18,6 +18,7 @@
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.isAltPressed
import androidx.compose.ui.input.key.isCtrlPressed
import androidx.compose.ui.input.key.isShiftPressed
import androidx.compose.ui.input.key.key
@@ -148,6 +149,12 @@
MappedKeys.MoveEnd -> KeyCommand.SELECT_END
else -> null
}
+ event.isAltPressed ->
+ when (event.key) {
+ MappedKeys.Backspace -> KeyCommand.DELETE_FROM_LINE_START
+ MappedKeys.Delete -> KeyCommand.DELETE_TO_LINE_END
+ else -> null
+ }
else -> null
} ?: common.map(event)
}
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index fe02b94..25ddd6e 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -84,6 +84,14 @@
</intent-filter>
</activity>
<activity
+ android:name=".LazyBoxWithConstraintsActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="androidx.compose.integration.macrobenchmark.target.LAZY_BOX_WITH_CONSTRAINTS_ACTIVITY" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ <activity
android:name=".DifferentTypesListActivity"
android:exported="true">
<intent-filter>
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt
new file mode 100644
index 0000000..d4714d9
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark-target/src/main/java/androidx/compose/integration/macrobenchmark/target/LazyBoxWithConstraintsActivity.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.integration.macrobenchmark.target
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+
+class LazyBoxWithConstraintsActivity : ComponentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 3000)
+ val items = List(itemCount) { entryIndex ->
+ NestedListEntry(
+ buildList {
+ repeat(10) {
+ add("${entryIndex}x$it")
+ }
+ }
+ )
+ }
+
+ setContent {
+ LazyColumn(
+ modifier = Modifier
+ .fillMaxWidth()
+ .semantics { contentDescription = "IamLazy" }
+ ) {
+ items(items) { entry ->
+ NonLazyRow(entry)
+ }
+ }
+ }
+
+ launchIdlenessTracking()
+ }
+
+ companion object {
+ const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+ }
+}
+
+@Composable
+private fun NonLazyRow(entry: NestedListEntry) {
+ BoxWithConstraints {
+ Row(
+ Modifier
+ .padding(16.dp)
+ .horizontalScroll(rememberScrollState())
+ ) {
+ entry.list.forEach {
+ Card(Modifier.size(80.dp)) {
+ Text(text = it)
+ }
+ Spacer(Modifier.size(16.dp))
+ }
+ }
+ }
+}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt
new file mode 100644
index 0000000..e19bc35
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/LazyBoxWithConstraintsScrollBenchmark.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.integration.macrobenchmark
+
+import android.content.Intent
+import android.graphics.Point
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.FrameTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import androidx.testutils.createCompilationParams
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class LazyBoxWithConstraintsScrollBenchmark(
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ private lateinit var device: UiDevice
+
+ @Before
+ fun setUp() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ device = UiDevice.getInstance(instrumentation)
+ }
+
+ @Test
+ fun start() {
+ benchmarkRule.measureRepeated(
+ packageName = PACKAGE_NAME,
+ metrics = listOf(FrameTimingMetric()),
+ compilationMode = compilationMode,
+ iterations = 10,
+ setupBlock = {
+ val intent = Intent()
+ intent.action = ACTION
+ startActivityAndWait(intent)
+ }
+ ) {
+ val lazyColumn = device.findObject(By.desc(CONTENT_DESCRIPTION))
+ // Setting a gesture margin is important otherwise gesture nav is triggered.
+ lazyColumn.setGestureMargin(device.displayWidth / 5)
+ for (i in 1..10) {
+ // From center we scroll 2/3 of it which is 1/3 of the screen.
+ lazyColumn.drag(Point(lazyColumn.visibleCenter.x, lazyColumn.visibleCenter.y / 3))
+ device.wait(Until.findObject(By.desc(COMPOSE_IDLE)), 3000)
+ }
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
+ private const val ACTION =
+ "androidx.compose.integration.macrobenchmark.target.LAZY_BOX_WITH_CONSTRAINTS_ACTIVITY"
+ private const val CONTENT_DESCRIPTION = "IamLazy"
+
+ private const val COMPOSE_IDLE = "COMPOSE-IDLE"
+
+ @Parameterized.Parameters(name = "compilation={0}")
+ @JvmStatic
+ fun parameters() = createCompilationParams()
+ }
+}
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/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
new file mode 100644
index 0000000..1cfa222
--- /dev/null
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransformTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.material.pullrefresh
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalMaterialApi::class)
+class PullRefreshIndicatorTransformTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ // Convert from floats to DP to avoid rounding issues later
+
+ private val IndicatorSize get() = with(rule.density) { 200f.toDp() }
+ // Make the box large enough so that when the indicator is offset when not shown, it is still
+ // offset to within the bounds of the containing box
+ private val ContainingBoxSize = with(rule.density) { 800f.toDp() }
+ private val BoxTag = "Box"
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorClippedWhenNotDisplayed() {
+ rule.setContent {
+ val state = rememberPullRefreshState(refreshing = false, {})
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be fully clipped and invisible
+ rule.onNodeWithTag(BoxTag).captureToImage().assertPixels { Color.White }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorPartiallyClippedWhenPartiallyDisplayed() {
+ lateinit var state: PullRefreshState
+ rule.setContent {
+ // Pull down by 100 pixels (the actual position delta is half of this because the state
+ // applies a multiplier)
+ state = rememberPullRefreshState(refreshing = false, onRefresh = {}).apply {
+ onPull(100f)
+ }
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be partially clipped
+ rule.onNodeWithTag(BoxTag).captureToImage().run {
+ val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorXRange = with(rule.density) {
+ indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+ }
+
+ val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorYRange = indicatorTop until indicatorTop + state.position.toInt()
+
+ val pixel = toPixelMap()
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+ Color.Black
+ } else {
+ Color.White
+ }
+ pixel.assertPixelColor(expectedColor, x, y)
+ }
+ }
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indicatorNotClippedWhenFullyDisplayed() {
+ lateinit var state: PullRefreshState
+ rule.setContent {
+ // Set refreshing and set the refreshing offset to match the indicator size -
+ // this means that the indicator will start to show at an offset of 0 from its normal
+ // layout position, since by default it is negatively offset by its height
+ state = rememberPullRefreshState(
+ refreshing = true,
+ onRefresh = {},
+ refreshingOffset = IndicatorSize
+ )
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color.White)
+ .wrapContentSize(Alignment.Center)
+ // Set a larger size so that when offset the indicator will still appear
+ .size(ContainingBoxSize)
+ .testTag(BoxTag),
+ contentAlignment = Alignment.Center
+ ) {
+ Box(Modifier
+ .pullRefreshIndicatorTransform(state)
+ .size(IndicatorSize)
+ .background(Color.Black)
+ )
+ }
+ }
+ // The indicator should be fully visible
+ rule.onNodeWithTag(BoxTag).captureToImage().run {
+ val indicatorStart = with(rule.density) { width / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorXRange = with(rule.density) {
+ indicatorStart until (indicatorStart + IndicatorSize.toPx()).toInt()
+ }
+
+ val indicatorTop = with(rule.density) { height / 2 - IndicatorSize.toPx() / 2 }.toInt()
+ val indicatorYRange = with(rule.density) {
+ indicatorTop until (indicatorTop + IndicatorSize.toPx()).toInt()
+ }
+
+ val pixel = toPixelMap()
+ for (x in 0 until width) {
+ for (y in 0 until height) {
+ val expectedColor = if (x in indicatorXRange && y in indicatorYRange) {
+ Color.Black
+ } else {
+ Color.White
+ }
+ pixel.assertPixelColor(expectedColor, x, y)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
index 63c1027..fe4190a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/pullrefresh/PullRefreshStateTest.kt
@@ -79,13 +79,14 @@
@Test
fun pullLessThanOrEqualToThreshold_doesNot_triggerRefresh() {
+ lateinit var state: PullRefreshState
var refreshCount = 0
var touchSlop = 0f
val threshold = 400f
rule.setContent {
touchSlop = LocalViewConfiguration.current.touchSlop
- val state = rememberPullRefreshState(
+ state = rememberPullRefreshState(
refreshing = false,
onRefresh = { refreshCount++ },
refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
@@ -110,7 +111,12 @@
// Equal to threshold
pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop) }
- rule.runOnIdle { assertThat(refreshCount).isEqualTo(0) }
+ rule.runOnIdle {
+ assertThat(refreshCount).isEqualTo(0)
+ // Since onRefresh was not called, we should reset the position back to 0
+ assertThat(state.progress).isEqualTo(0f)
+ assertThat(state.position).isEqualTo(0f)
+ }
}
@Test
@@ -309,6 +315,43 @@
}
}
+ @Test
+ fun pullBeyondThreshold_refreshingNotChangedToTrue_animatePositionBackToZero() {
+ lateinit var state: PullRefreshState
+ var refreshCount = 0
+ var touchSlop = 0f
+ val threshold = 400f
+
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ state = rememberPullRefreshState(
+ refreshing = false,
+ onRefresh = { refreshCount++ },
+ refreshThreshold = with(LocalDensity.current) { threshold.toDp() }
+ )
+
+ Box(Modifier.pullRefresh(state).testTag(PullRefreshTag)) {
+ LazyColumn {
+ items(100) {
+ Text("item $it")
+ }
+ }
+ }
+ }
+
+ // Account for PullModifier - pull down twice the threshold value.
+ pullRefreshNode.performTouchInput { swipeDown(endY = 2 * threshold + touchSlop + 1f) }
+
+ rule.runOnIdle {
+ // onRefresh should be called
+ assertThat(refreshCount).isEqualTo(1)
+ // Since onRefresh did not change refreshing, we should have reset the position back to
+ // 0
+ assertThat(state.progress).isEqualTo(0f)
+ assertThat(state.position).isEqualTo(0f)
+ }
+ }
+
/**
* Taken from the private function of the same name in [PullRefreshState].
*/
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index 9ee4ffb..21d797a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -63,7 +63,7 @@
*
* @param state The associated [SwipeableV2State].
* @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
* @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
* swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
index 7ca9609..ad97176 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshIndicatorTransform.kt
@@ -18,15 +18,12 @@
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.material.ExperimentalMaterialApi
-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.composed
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.platform.inspectable
/**
* A modifier for translating the position and scaling the size of a pull-to-refresh indicator
@@ -42,17 +39,30 @@
fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState,
scale: Boolean = false,
-) = composed(inspectorInfo = debugInspectorInfo {
+) = inspectable(inspectorInfo = debugInspectorInfo {
name = "pullRefreshIndicatorTransform"
properties["state"] = state
properties["scale"] = scale
}) {
- var height by remember { mutableStateOf(0) }
-
Modifier
- .onSizeChanged { height = it.height }
+ // Essentially we only want to clip the at the top, so the indicator will not appear when
+ // the position is 0. It is preferable to clip the indicator as opposed to the layout that
+ // contains the indicator, as this would also end up clipping shadows drawn by items in a
+ // list for example - so we leave the clipping to the scrolling container. We use MAX_VALUE
+ // for the other dimensions to allow for more room for elevation / arbitrary indicators - we
+ // only ever really want to clip at the top edge.
+ .drawWithContent {
+ clipRect(
+ top = 0f,
+ left = -Float.MAX_VALUE,
+ right = Float.MAX_VALUE,
+ bottom = Float.MAX_VALUE
+ ) {
+ this@drawWithContent.drawContent()
+ }
+ }
.graphicsLayer {
- translationY = state.position - height
+ translationY = state.position - size.height
if (scale && !state.refreshing) {
val scaleFraction = LinearOutSlowInEasing
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
index e673400..b425a38 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/pullrefresh/PullRefreshState.kt
@@ -17,6 +17,7 @@
package androidx.compose.material.pullrefresh
import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatorMutex
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -136,9 +137,8 @@
if (!this._refreshing) {
if (adjustedDistancePulled > threshold) {
onRefreshState.value()
- } else {
- animateIndicatorTo(0f)
}
+ animateIndicatorTo(0f)
}
distancePulled = 0f
}
@@ -151,9 +151,16 @@
}
}
+ // Make sure to cancel any existing animations when we launch a new one. We use this instead of
+ // Animatable as calling snapTo() on every drag delta has a one frame delay, and some extra
+ // overhead of running through the animation pipeline instead of directly mutating the state.
+ private val mutatorMutex = MutatorMutex()
+
private fun animateIndicatorTo(offset: Float) = animationScope.launch {
- animate(initialValue = _position, targetValue = offset) { value, _ ->
- _position = value
+ mutatorMutex.mutate {
+ animate(initialValue = _position, targetValue = offset) { value, _ ->
+ _position = value
+ }
}
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index 4849ec9..021de9a 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -35,8 +35,15 @@
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
@@ -46,19 +53,19 @@
import kotlinx.coroutines.launch
/**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
*
* When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
* delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
* When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
* the new anchor.
*
* Swiping is constrained between the minimum and maximum anchors.
*
* @param state The associated [SwipeableV2State].
* @param orientation The orientation in which the swipeable can be swiped.
- * @param enabled Whether this [swipeable] is enabled and should react to the user's input.
+ * @param enabled Whether this [swipeableV2] is enabled and should react to the user's input.
* @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom
* swipe will behave like bottom to top, and a left to right swipe will behave like right to left.
* @param interactionSource Optional [MutableInteractionSource] that will passed on to
@@ -86,36 +93,46 @@
* the state with them.
*
* @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
* @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
* Components with custom reconciliation logic should implement this callback, i.e. to re-target an
* in-progress animation.
* @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
*/
@ExperimentalMaterial3Api
internal fun <T> Modifier.swipeAnchors(
state: SwipeableV2State<T>,
- possibleStates: Set<T>,
+ possibleValues: Set<T>,
anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
- calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
- val previousAnchors = state.anchors
- val newAnchors = mutableMapOf<T, Float>()
- possibleStates.forEach {
- val anchorValue = calculateAnchor(it, layoutSize)
- if (anchorValue != null) {
- newAnchors[it] = anchorValue
+ calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
+) = this.then(SwipeAnchorsModifier(
+ onDensityChanged = { state.density = it },
+ onSizeChanged = { layoutSize ->
+ val previousAnchors = state.anchors
+ val newAnchors = mutableMapOf<T, Float>()
+ possibleValues.forEach {
+ val anchorValue = calculateAnchor(it, layoutSize)
+ if (anchorValue != null) {
+ newAnchors[it] = anchorValue
+ }
}
+ if (previousAnchors != newAnchors) {
+ state.updateAnchors(newAnchors)
+ if (previousAnchors.isNotEmpty()) {
+ anchorsChanged?.invoke(previousAnchors, newAnchors)
+ }
+ }
+ },
+ inspectorInfo = debugInspectorInfo {
+ name = "swipeAnchors"
+ properties["state"] = state
+ properties["possibleValues"] = possibleValues
+ properties["anchorsChanged"] = anchorsChanged
+ properties["calculateAnchor"] = calculateAnchor
}
- if (previousAnchors == newAnchors) return@onSizeChanged
- state.updateAnchors(newAnchors)
-
- if (previousAnchors.isNotEmpty()) {
- anchorsChanged?.invoke(previousAnchors, newAnchors)
- }
-}
+))
/**
* State of the [swipeableV2] modifier.
@@ -124,10 +141,9 @@
* to change the state either immediately or by starting an animation. To create and remember a
* [SwipeableV2State] use [rememberSwipeableV2State].
*
- * @param initialState The initial value of the state.
- * @param density The density used to convert thresholds from px to dp.
+ * @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
* the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -140,31 +156,30 @@
@Stable
@ExperimentalMaterial3Api
internal class SwipeableV2State<T>(
- initialState: T,
- internal val density: Density,
+ initialValue: T,
internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+ internal val confirmValueChange: (newValue: T) -> Boolean = { true },
internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
SwipeableV2Defaults.PositionalThreshold,
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
/**
- * The current state of the [SwipeableV2State].
+ * The current value of the [SwipeableV2State].
*/
- var currentState: T by mutableStateOf(initialState)
+ var currentValue: T by mutableStateOf(initialValue)
private set
/**
- * The target state. This is the closest state to the current offset (taking into account
+ * The target value. This is the closest value to the current offset (taking into account
* positional thresholds). If no interactions like animations or drags are in progress, this
- * will be the current state.
+ * will be the current value.
*/
- val targetState: T by derivedStateOf {
+ val targetValue: T by derivedStateOf {
val currentOffset = offset
if (currentOffset != null) {
- computeTarget(currentOffset, currentState, velocity = 0f)
- } else currentState
+ computeTarget(currentOffset, currentValue, velocity = 0f)
+ } else currentValue
}
/**
@@ -180,6 +195,7 @@
*
* To guarantee stricter semantics, consider using [requireOffset].
*/
+ @get:Suppress("AutoBoxing")
val offset: Float? by derivedStateOf {
dragPosition?.coerceIn(minBound, maxBound)
}
@@ -201,12 +217,13 @@
private set
/**
- * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+ * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+ * bounds.
*/
/*@FloatRange(from = 0f, to = 1f)*/
val progress: Float by derivedStateOf {
- val a = anchors[currentState] ?: 0f
- val b = anchors[targetState] ?: 0f
+ val a = anchors[currentValue] ?: 0f
+ val b = anchors[targetValue] ?: 0f
val distance = abs(b - a)
if (distance > 1e-6f) {
val progress = (this.requireOffset() - a) / (b - a)
@@ -229,57 +246,57 @@
private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
- private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
internal val draggableState = DraggableState {
dragPosition = (dragPosition ?: 0f) + it
}
internal var anchors by mutableStateOf(emptyMap<T, Float>())
+ internal var density: Density? = null
+
internal fun updateAnchors(newAnchors: Map<T, Float>) {
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
if (previousAnchorsEmpty) {
- dragPosition = anchors.requireAnchor(this.currentState)
+ dragPosition = anchors.requireAnchor(this.currentValue)
}
}
/**
- * Whether the [state] has an anchor associated with it.
+ * Whether the [value] has an anchor associated with it.
*/
- fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+ fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
/**
- * Snap to a [targetState] without any animation.
+ * Snap to a [targetValue] without any animation.
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
*/
- suspend fun snapTo(targetState: T) {
- val targetOffset = anchors.requireAnchor(targetState)
+ suspend fun snapTo(targetValue: T) {
+ val targetOffset = anchors.requireAnchor(targetValue)
draggableState.drag {
dragBy(targetOffset - requireOffset())
}
- this.currentState = targetState
+ this.currentValue = targetValue
}
/**
- * Animate to a [targetState].
+ * Animate to a [targetValue].
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
* @param velocity The velocity the animation should start with, [lastVelocity] by default
*/
suspend fun animateTo(
- targetState: T,
+ targetValue: T,
velocity: Float = lastVelocity,
) {
- val targetOffset = anchors.requireAnchor(targetState)
+ val targetOffset = anchors.requireAnchor(targetValue)
try {
draggableState.drag {
isAnimationRunning = true
@@ -304,7 +321,7 @@
.entries
.firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
?.key
- this.currentState = endState ?: currentState
+ this.currentValue = endState ?: currentValue
}
}
@@ -312,17 +329,17 @@
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
suspend fun settle(velocity: Float) {
- val previousState = this.currentState
- val targetState = computeTarget(
+ val previousValue = this.currentValue
+ val targetValue = computeTarget(
offset = requireOffset(),
- currentState = previousState,
+ currentValue = previousValue,
velocity = velocity
)
- if (confirmStateChange(targetState)) {
- animateTo(targetState, velocity)
+ if (confirmValueChange(targetValue)) {
+ animateTo(targetValue, velocity)
} else {
// If the user vetoed the state change, rollback to the previous state.
- animateTo(previousState, velocity)
+ animateTo(previousValue, velocity)
}
}
@@ -344,42 +361,49 @@
private fun computeTarget(
offset: Float,
- currentState: T,
+ currentValue: T,
velocity: Float
): T {
val currentAnchors = anchors
- val currentAnchor = currentAnchors.requireAnchor(currentState)
+ val currentAnchor = currentAnchors.requireAnchor(currentValue)
+ val currentDensity = requireDensity()
+ val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
return if (currentAnchor <= offset) {
// Swiping from lower to upper (positive).
if (velocity >= velocityThresholdPx) {
- currentAnchors.closestState(offset, true)
+ currentAnchors.closestAnchor(offset, true)
} else {
- val upper = currentAnchors.closestState(offset, true)
+ val upper = currentAnchors.closestAnchor(offset, true)
val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor + relativeThreshold)
- if (offset < absoluteThreshold) currentState else upper
+ if (offset < absoluteThreshold) currentValue else upper
}
} else {
// Swiping from upper to lower (negative).
if (velocity <= -velocityThresholdPx) {
- currentAnchors.closestState(offset, false)
+ currentAnchors.closestAnchor(offset, false)
} else {
- val lower = currentAnchors.closestState(offset, false)
+ val lower = currentAnchors.closestAnchor(offset, false)
val distance = abs(currentAnchor - currentAnchors.getValue(lower))
- val relativeThreshold = abs(positionalThreshold(density, distance))
+ val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
val absoluteThreshold = abs(currentAnchor - relativeThreshold)
if (offset < 0) {
// For negative offsets, larger absolute thresholds are closer to lower anchors
// than smaller ones.
- if (abs(offset) < absoluteThreshold) currentState else lower
+ if (abs(offset) < absoluteThreshold) currentValue else lower
} else {
- if (offset > absoluteThreshold) currentState else lower
+ if (offset > absoluteThreshold) currentValue else lower
}
}
}
}
+ private fun requireDensity() = requireNotNull(density) {
+ "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+ "this=$this SwipeableState?"
+ }
+
companion object {
/**
* The default [Saver] implementation for [SwipeableV2State].
@@ -387,20 +411,18 @@
@ExperimentalMaterial3Api
fun <T : Any> Saver(
animationSpec: AnimationSpec<Float>,
- confirmStateChange: (T) -> Boolean,
+ confirmValueChange: (T) -> Boolean,
positionalThreshold: Density.(distance: Float) -> Float,
- velocityThreshold: Dp,
- density: Density
+ velocityThreshold: Dp
) = Saver<SwipeableV2State<T>, T>(
- save = { it.currentState },
+ save = { it.currentValue },
restore = {
SwipeableV2State(
- initialState = it,
+ initialValue = it,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
- velocityThreshold = velocityThreshold,
- density = density
+ velocityThreshold = velocityThreshold
)
}
)
@@ -410,35 +432,32 @@
/**
* Create and remember a [SwipeableV2State].
*
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
*/
@Composable
@ExperimentalMaterial3Api
internal fun <T : Any> rememberSwipeableV2State(
- initialState: T,
+ initialValue: T,
animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
+ confirmValueChange: (newValue: T) -> Boolean = { true }
): SwipeableV2State<T> {
- val density = LocalDensity.current
return rememberSaveable(
- initialState, animationSpec, confirmStateChange, density,
+ initialValue, animationSpec, confirmValueChange,
saver = SwipeableV2State.Saver(
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
),
) {
SwipeableV2State(
- initialState = initialState,
+ initialValue = initialValue,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
- velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
- density = density
+ velocityThreshold = SwipeableV2Defaults.VelocityThreshold
)
}
}
@@ -492,11 +511,42 @@
fixedPositionalThreshold(56.dp)
}
-private fun <T> Map<T, Float>.closestState(
+@Stable
+private class SwipeAnchorsModifier(
+ private val onDensityChanged: (density: Density) -> Unit,
+ private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+ inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+ private var lastDensity: Float = -1f
+ private var lastFontScale: Float = -1f
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ if (density != lastDensity || fontScale != lastFontScale) {
+ onDensityChanged(Density(density, fontScale))
+ lastDensity = density
+ lastFontScale = fontScale
+ }
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+ }
+
+ override fun onRemeasured(size: IntSize) {
+ onSizeChanged(size)
+ }
+
+ override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+ "onSizeChanged=$onSizeChanged)"
+}
+
+private fun <T> Map<T, Float>.closestAnchor(
offset: Float = 0f,
searchUpwards: Boolean = false
): T {
- require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+ require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
return minBy { (_, anchor) ->
val delta = if (searchUpwards) anchor - offset else offset - anchor
if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -505,6 +555,6 @@
private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
- "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+ "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
}
diff --git a/compose/ui/ui-tooling-preview/api/current.txt b/compose/ui/ui-tooling-preview/api/current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/current.txt
+++ b/compose/ui/ui-tooling-preview/api/current.txt
@@ -48,6 +48,7 @@
method public abstract boolean showBackground() default false;
method public abstract boolean showSystemUi() default false;
method public abstract int uiMode() default 0;
+ method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
method public abstract int widthDp() default -1;
property public abstract int apiLevel;
property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
property public abstract boolean showBackground;
property public abstract boolean showSystemUi;
property public abstract int uiMode;
+ property public abstract int wallpaper;
property public abstract int widthDp;
}
@@ -81,6 +83,15 @@
property public abstract kotlin.sequences.Sequence<T> values;
}
+ public final class Wallpapers {
+ field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+ field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+ field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+ field public static final int NONE = -1; // 0xffffffff
+ field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+ field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+ }
+
}
package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-preview/api/public_plus_experimental_current.txt
@@ -48,6 +48,7 @@
method public abstract boolean showBackground() default false;
method public abstract boolean showSystemUi() default false;
method public abstract int uiMode() default 0;
+ method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
method public abstract int widthDp() default -1;
property public abstract int apiLevel;
property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
property public abstract boolean showBackground;
property public abstract boolean showSystemUi;
property public abstract int uiMode;
+ property public abstract int wallpaper;
property public abstract int widthDp;
}
@@ -81,6 +83,15 @@
property public abstract kotlin.sequences.Sequence<T> values;
}
+ public final class Wallpapers {
+ field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+ field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+ field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+ field public static final int NONE = -1; // 0xffffffff
+ field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+ field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+ }
+
}
package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/api/restricted_current.txt b/compose/ui/ui-tooling-preview/api/restricted_current.txt
index 8cdeeb4..01e7297 100644
--- a/compose/ui/ui-tooling-preview/api/restricted_current.txt
+++ b/compose/ui/ui-tooling-preview/api/restricted_current.txt
@@ -48,6 +48,7 @@
method public abstract boolean showBackground() default false;
method public abstract boolean showSystemUi() default false;
method public abstract int uiMode() default 0;
+ method public abstract int wallpaper() default androidx.compose.ui.tooling.preview.Wallpapers.NONE;
method public abstract int widthDp() default -1;
property public abstract int apiLevel;
property public abstract long backgroundColor;
@@ -60,6 +61,7 @@
property public abstract boolean showBackground;
property public abstract boolean showSystemUi;
property public abstract int uiMode;
+ property public abstract int wallpaper;
property public abstract int widthDp;
}
@@ -81,6 +83,15 @@
property public abstract kotlin.sequences.Sequence<T> values;
}
+ public final class Wallpapers {
+ field public static final int BLUE_DOMINATED_EXAMPLE = 2; // 0x2
+ field public static final int GREEN_DOMINATED_EXAMPLE = 1; // 0x1
+ field public static final androidx.compose.ui.tooling.preview.Wallpapers INSTANCE;
+ field public static final int NONE = -1; // 0xffffffff
+ field public static final int RED_DOMINATED_EXAMPLE = 0; // 0x0
+ field public static final int YELLOW_DOMINATED_EXAMPLE = 3; // 0x3
+ }
+
}
package androidx.compose.ui.tooling.preview.datasource {
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
index f581bf0..c064824 100644
--- a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
@@ -51,6 +51,8 @@
* @param uiMode Bit mask of the ui mode as per [android.content.res.Configuration.uiMode]
* @param device Device string indicating the device to use in the preview. See the available
* devices in [Devices].
+ * @param wallpaper Integer defining which wallpaper from those available in Android Studio
+ * to use for dynamic theming.
*/
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@@ -73,5 +75,6 @@
val showBackground: Boolean = false,
val backgroundColor: Long = 0,
@UiMode val uiMode: Int = 0,
- @Device val device: String = Devices.DEFAULT
+ @Device val device: String = Devices.DEFAULT,
+ @Wallpaper val wallpaper: Int = Wallpapers.NONE,
)
diff --git a/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.kt
new file mode 100644
index 0000000..4dbc0972
--- /dev/null
+++ b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Wallpaper.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.compose.ui.tooling.preview
+
+import androidx.annotation.IntDef
+
+/**
+ * Wallpapers available to be used in the [Preview].
+ */
+object Wallpapers {
+ /** Default value, representing dynamic theming not enabled. */
+ const val NONE = -1
+ /** Example wallpaper whose dominant colour is red. */
+ const val RED_DOMINATED_EXAMPLE = 0
+ /** Example wallpaper whose dominant colour is green. */
+ const val GREEN_DOMINATED_EXAMPLE = 1
+ /** Example wallpaper whose dominant colour is blue. */
+ const val BLUE_DOMINATED_EXAMPLE = 2
+ /** Example wallpaper whose dominant colour is yellow. */
+ const val YELLOW_DOMINATED_EXAMPLE = 3
+}
+
+/**
+ * Annotation for defining the wallpaper to use for dynamic theming in the [Preview].
+ * @suppress
+ */
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(Wallpapers.NONE, Wallpapers.RED_DOMINATED_EXAMPLE, Wallpapers.GREEN_DOMINATED_EXAMPLE,
+ Wallpapers.BLUE_DOMINATED_EXAMPLE, Wallpapers.YELLOW_DOMINATED_EXAMPLE)
+annotation class Wallpaper
diff --git a/compose/ui/ui-tooling/OWNERS b/compose/ui/ui-tooling/OWNERS
index 1211f02..32894f6 100644
--- a/compose/ui/ui-tooling/OWNERS
+++ b/compose/ui/ui-tooling/OWNERS
@@ -2,4 +2,5 @@
diegoperez@google.com
jlauridsen@google.com
amaurym@google.com
-kudasov@google.com
\ No newline at end of file
+kudasov@google.com
+kseniia@google.com
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index e99e3fb..05afe97 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -301,6 +301,16 @@
)
}
+ @Test
+ fun materialAnimationsAreSubscribed() {
+ checkAnimationsAreSubscribed(
+ "MaterialPreview",
+ unsupported = emptyList(),
+ transitions = listOf("ToggleableState"),
+ animateXAsState = listOf("ColorAnimation", "ColorAnimation", "ColorAnimation")
+ )
+ }
+
private fun checkAnimationsAreSubscribed(
preview: String,
unsupported: List<String> = emptyList(),
@@ -420,6 +430,19 @@
)
}
+ /**
+ * Verifies the use of inline classes as preview default parameters. Methods with inline
+ * classes as parameters will get the name mangled so we need to ensure we invoke correctly
+ * the right method.
+ */
+ @Test
+ fun defaultParametersComposableTest4() {
+ assertRendersCorrectly(
+ "androidx.compose.ui.tooling.SimpleComposablePreviewKt",
+ "DefaultParametersPreview4"
+ )
+ }
+
@Test
fun previewInClass() {
assertRendersCorrectly(
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
index 9c8819b..eabc75a 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
@@ -76,6 +76,14 @@
@Preview
@Composable
+fun DefaultParametersPreview4(a: String = "Hello", b: Color = Color.White) {
+ if (a != "Hello") throw IllegalArgumentException("Unexpected default value")
+ if (b != Color.White) throw IllegalArgumentException("Unexpected default value")
+ Text("Default parameter $a $b")
+}
+
+@Preview
+@Composable
private fun LifecyclePreview() {
val lifecycleState = LocalLifecycleOwner.current.lifecycle.currentState
if (lifecycleState != Lifecycle.State.RESUMED) throw IllegalArgumentException(
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
index b52329d..5debf14 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
@@ -56,6 +56,7 @@
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
+import androidx.compose.material.TriStateCheckbox
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.runtime.Composable
@@ -69,6 +70,7 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -422,4 +424,11 @@
}
.background(Color.Gray, shape = CircleShape)
)
+}
+
+@Preview
+@Composable
+fun MaterialPreview() {
+ val state = remember { mutableStateOf(ToggleableState.On) }.value
+ TriStateCheckbox(state, {})
}
\ No newline at end of file
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..1e0c7d7 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
@@ -152,14 +152,14 @@
assertEquals(0, it.startTimeMillis)
assertEquals(330, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
- assertEquals(5, it.values.size)
+ assertGreaterThanOrEqualTo(4, it.values.size)
}
transitions[1].let {
assertEquals("Built-in shrink/expand", it.label)
assertEquals(0, it.startTimeMillis)
assertEquals(350, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
- assertEquals(5, it.values.size)
+ assertGreaterThanOrEqualTo(4, it.values.size)
}
transitions[2].let {
assertEquals("Built-in InterruptionHandlingOffset", it.label)
@@ -201,7 +201,7 @@
assertEquals(0, it.startTimeMillis)
assertEquals(300, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
- assertEquals(4, it.values.size)
+ assertGreaterThanOrEqualTo(3, it.values.size)
}
transitions[0].values.let {
assertTrue(it.containsKey(0L))
@@ -240,21 +240,21 @@
assertEquals(0, it.startTimeMillis)
assertEquals(190, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.SpringSpec", it.specType)
- assertEquals(3, it.values.size)
+ assertGreaterThanOrEqualTo(3, it.values.size)
}
transitions[1].let {
assertEquals("Built-in alpha", it.label)
assertEquals(90, it.startTimeMillis, 30)
assertEquals(310, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
- assertEquals(4, it.values.size)
+ assertGreaterThanOrEqualTo(3, it.values.size)
}
transitions[2].let {
assertEquals("Built-in scale", it.label)
assertEquals(90, it.startTimeMillis, 30)
assertEquals(310, it.endTimeMillis, 30)
assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
- assertEquals(4, it.values.size)
+ assertGreaterThanOrEqualTo(3, it.values.size)
}
transitions[3].let {
// This animation probably will be removed.
@@ -498,6 +498,10 @@
assertEquals(null, expected.toFloat(), actual.toFloat(), delta.toFloat())
}
+ private fun assertGreaterThanOrEqualTo(min: Int, actual: Int) {
+ assertTrue(actual >= min)
+ }
+
fun assertEquals(expected: Long, actual: Long, delta: Long) {
assertEquals(null, expected.toFloat(), actual.toFloat(), delta.toFloat())
}
diff --git a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
index 04b7bfd..24f2f87 100644
--- a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
@@ -52,7 +52,10 @@
): Method {
val actualTypes: Array<Class<*>> = arrayOf(*args)
return declaredMethods.firstOrNull {
- methodName == it.name && compatibleTypes(it.parameterTypes, actualTypes)
+ // Methods with inlined classes as parameter will have the name mangled
+ // so we need to check for methodName-xxxx as well
+ (methodName == it.name || it.name.startsWith("$methodName-")) &&
+ compatibleTypes(it.parameterTypes, actualTypes)
} ?: throw NoSuchMethodException("$methodName not found")
}
@@ -72,11 +75,16 @@
methodName,
*args.mapNotNull { it?.javaClass }.toTypedArray(),
Composer::class.java, // composer param
- *kotlin.Int::class.java.dup(changedParams) // changed params
+ *kotlin.Int::class.java.dup(changedParams) // changed param
)
} catch (e: ReflectiveOperationException) {
try {
- declaredMethods.find { it.name == methodName }
+ declaredMethods.find {
+ it.name == methodName ||
+ // Methods with inlined classes as parameter will have the name mangled
+ // so we need to check for methodName-xxxx as well
+ it.name.startsWith("$methodName-")
+ }
} catch (e: ReflectiveOperationException) {
null
}
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/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index f3a63ae..52f73d2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -2035,13 +2035,27 @@
}
}
+ var precomposedSlotActive = false
+
val handle = rule.runOnIdle {
state.precompose(1) {
- Box(modifier = Modifier.size(10.dp).testTag("1"))
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .testTag("1")
+ )
+
+ DisposableEffect(Unit) {
+ precomposedSlotActive = true
+ onDispose {
+ precomposedSlotActive = false
+ }
+ }
}
}
rule.runOnIdle {
+ assertThat(precomposedSlotActive).isTrue()
needSlot = false
}
@@ -2049,7 +2063,10 @@
handle.dispose()
}
- rule.onNodeWithTag("1").assertDoesNotExist()
+ assertThat(precomposedSlotActive).isFalse()
+
+ // Both slots inside subcompose are reused, as parent was detached with these nodes active
+ rule.onNodeWithTag("1").assertIsNotDisplayed()
rule.onNodeWithTag("0").assertIsNotDisplayed()
}
@@ -2168,6 +2185,149 @@
rule.waitUntil { isActive }
}
+ @Test
+ fun reusingNestedSubcompose_nestedChildrenAreResetAndReused() {
+ val slotState = mutableStateOf(0)
+
+ val activeChildren = mutableSetOf<Int>()
+ var remeasureCount = 0
+ val measureCountModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ rule.setContent {
+ SubcomposeLayout(
+ remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
+ ) { constraints ->
+ val slot = slotState.value
+ val child = measure(slot, constraints) {
+ Box {
+ SubcomposeLayout { constraints ->
+ val placeable = measure(Unit, constraints) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .then(measureCountModifier)
+ )
+
+ DisposableEffect(Unit) {
+ activeChildren += slot
+ onDispose {
+ activeChildren -= slot
+ }
+ }
+ }
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+ }
+ layout(child.width, child.height) {
+ child.place(0, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(activeChildren).containsExactly(0)
+
+ slotState.value = 1
+ }
+
+ rule.runOnIdle {
+ assertThat(activeChildren).containsExactly(1)
+ assertThat(remeasureCount).isEqualTo(2)
+
+ remeasureCount = 0
+
+ slotState.value = 2
+ }
+
+ rule.runOnIdle {
+ assertThat(activeChildren).containsExactly(2)
+ assertThat(remeasureCount).isEqualTo(0)
+ }
+ }
+
+ @Test
+ fun reusingNestedSubcompose_nestedContentIsResetWhenReusedOnNextFrame() {
+ var contentActive by mutableStateOf(true)
+ var slotId by mutableStateOf(0)
+ val activeChildren = mutableSetOf<Int>()
+ var remeasureCount = 0
+ val measureCountModifier = Modifier.layout { measurable, constraints ->
+ remeasureCount++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ rule.setContent {
+ SubcomposeLayout(
+ remember { SubcomposeLayoutState(SubcomposeSlotReusePolicy(1)) }
+ ) { constraints ->
+ if (contentActive) {
+ val child = measure(slotId, constraints) {
+ Box {
+ SubcomposeLayout { constraints ->
+ val placeable = measure(Unit, constraints) {
+ Box(modifier = Modifier.size(10.dp).then(measureCountModifier))
+
+ DisposableEffect(Unit) {
+ val capturedSlotId = slotId
+ activeChildren += slotId
+ onDispose {
+ activeChildren -= capturedSlotId
+ }
+ }
+ }
+ layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+ }
+
+ DisposableEffect(Unit) {
+ onDispose {
+ // schedule remeasure / compose when child is reset
+ contentActive = true
+ slotId++
+ }
+ }
+ }
+ layout(child.width, child.height) {
+ child.place(0, 0)
+ }
+ } else {
+ layout(0, 0) { }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(activeChildren).containsExactly(0)
+
+ contentActive = false
+ }
+
+ rule.runOnIdle {
+ assertThat(activeChildren).containsExactly(1)
+ }
+ }
+
+ private fun SubcomposeMeasureScope.measure(
+ slotId: Any,
+ constraints: Constraints,
+ content: @Composable () -> Unit
+ ): Placeable =
+ subcompose(slotId, content).first().measure(constraints)
+
private fun composeItems(
state: SubcomposeLayoutState,
items: MutableState<List<Int>>
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 02c2c59..65fe4f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -558,6 +558,7 @@
assertNotNull("test did not run", result?.getOrThrow())
}
+ @Ignore // b/260006789
@Test
fun canScrollVerticallyDown_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
@@ -583,6 +584,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollVerticallyUp_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
@@ -610,6 +612,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollVertically_returnsFalse_afterDownEventOutsideScrollable() {
lateinit var composeView: View
@@ -633,6 +636,7 @@
}
}
+ @Ignore // b/260006789
@Test
fun canScrollHorizontallyRight_returnsTrue_onlyAfterDownEventInScrollable() {
lateinit var composeView: View
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index f3ce5e4..164b02e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -449,7 +449,7 @@
)
/**
- * Determines when to rendering the contents of the buffer into an offscreen layer before
+ * Determines when to render the contents of a layer into an offscreen buffer before
* being drawn to the destination.
*/
@Immutable
@@ -467,7 +467,9 @@
* a compositing layer is created automatically to first render the contents fully opaque,
* then draw this offscreen buffer to the destination with the corresponding alpha. This is
* necessary for correctness otherwise alpha applied to individual drawing instructions that
- * overlap will have a different result than expected
+ * overlap will have a different result than expected. Additionally usage of [RenderEffect]
+ * on the graphicsLayer will also render into an intermediate offscreen buffer before
+ * being drawn into the destination.
*/
val Auto = CompositingStrategy(0)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
index 406a1c2..381d62c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorCompose.kt
@@ -21,9 +21,27 @@
import androidx.compose.runtime.ComposeNode
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.PathMeasure
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.drawscope.Stroke
+/**
+ * Defines a group of [Path]s and other [Group]s inside a [VectorPainter]. This is not a regular UI
+ * composable, it can only be called inside composables called from the content parameter to
+ * [rememberVectorPainter].
+ *
+ * @param name Optional name of the group used when describing the vector as a string.
+ * @param rotation The rotation of the group around the Z axis, in degrees.
+ * @param pivotX The horizontal pivot point used for rotation, in pixels.
+ * @param pivotY The vertical pivot point used for rotation, in pixels.
+ * @param scaleX Factor to scale the group by horizontally.
+ * @param scaleY Factor to scale the group by vertically.
+ * @param translationX Horizontal offset of the group, in pixels.
+ * @param translationY Vertical offset of the group, in pixels.
+ * @param clipPathData A list of [PathNode]s that define how to clip the group. Empty by default.
+ * @param content A composable that defines the contents of the group.
+ */
@Composable
@VectorComposable
fun Group(
@@ -56,6 +74,27 @@
}
}
+/**
+ * Defines a path inside a [VectorPainter]. This is not a regular UI composable, it can only be
+ * called inside composables called from the content parameter to [rememberVectorPainter].
+ *
+ * @param pathData List of [PathNode]s that define the path.
+ * @param pathFillType The [PathFillType] that specifies how to fill the path.
+ * @param name Optional name of the path used when describing the vector as a string.
+ * @param fill The [Brush] used to fill the path.
+ * @param fillAlpha The alpha value to use for [fill].
+ * @param stroke The [Brush] used to stroke the path.
+ * @param strokeAlpha The alpha value to use for [stroke].
+ * @param strokeLineWidth The width of the [stroke]. See [Stroke.width] for details.
+ * @param strokeLineCap The [StrokeCap] of [stroke]. See [Stroke.cap] for details.
+ * @param strokeLineJoin The [StrokeJoin] of [stroke]. See [Stroke.join] for details.
+ * @param strokeLineMiter The stroke miter value. See [Stroke.miter] for details.
+ * @param trimPathStart The fraction of the path that specifies the start of the clipped region of
+ * the path. See [PathMeasure.getSegment].
+ * @param trimPathEnd The fraction of the path that specifies the end of the clipped region of the
+ * path. See [PathMeasure.getSegment].
+ * @param trimPathOffset The amount to offset both [trimPathStart] and [trimPathEnd].
+ */
@Composable
@VectorComposable
fun Path(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index 8a0a4c9..3963cc0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -94,8 +94,9 @@
)
/**
- * Create a [VectorPainter] with the Vector defined by the provided
- * sub-composition
+ * Create a [VectorPainter] with the Vector defined by the provided sub-composition.
+ *
+ * Inside [content] use the [Group] and [Path] composables to define the vector.
*
* @param [defaultWidth] Intrinsic width of the Vector in [Dp]
* @param [defaultHeight] Intrinsic height of the Vector in [Dp]
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 575a9ba..1196016 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -18,12 +18,13 @@
import androidx.compose.runtime.Applier
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ComposeNode
import androidx.compose.runtime.Composition
import androidx.compose.runtime.CompositionContext
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ReusableComposeNode
import androidx.compose.runtime.ReusableContentHost
import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.collection.mutableVectorOf
import androidx.compose.runtime.currentComposer
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -33,8 +34,8 @@
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.UiComposable
+import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle
import androidx.compose.ui.materialize
import androidx.compose.ui.node.ComposeUiNode
import androidx.compose.ui.node.LayoutNode
@@ -111,7 +112,7 @@
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
- ComposeNode<LayoutNode, Applier<Any>>(
+ ReusableComposeNode<LayoutNode, Applier<Any>>(
factory = LayoutNode.Constructor,
update = {
set(state, state.setRoot)
@@ -130,8 +131,9 @@
}
val stateHolder = rememberUpdatedState(state)
DisposableEffect(Unit) {
+ stateHolder.value.keepDetachedNodes()
onDispose {
- stateHolder.value.disposeCurrentNodes()
+ stateHolder.value.disposeOrReuseChildren()
}
}
}
@@ -225,9 +227,11 @@
fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle =
state.precompose(slotId, content)
+ internal fun keepDetachedNodes() = state.keepDetachedNodes()
+
internal fun forceRecomposeChildren() = state.forceRecomposeChildren()
- internal fun disposeCurrentNodes() = state.disposeCurrentNodes()
+ internal fun disposeOrReuseChildren() = state.disposeOrReuseChildren()
/**
* Instance of this interface is returned by [precompose] function.
@@ -364,14 +368,16 @@
if (field !== value) {
field = value
// apply the new policy
- disposeOrReuseStartingFromIndex(0)
+ disposeOrReuseStartingFromIndex(0, retainAllSlots = true)
}
}
private var currentIndex = 0
+ private var disposeOnDetach = false
private val nodeToNodeState = mutableMapOf<LayoutNode, NodeState>()
// this map contains active slotIds (without precomposed or reusable nodes)
private val slotIdToNode = mutableMapOf<Any?, LayoutNode>()
+ private val detachedNodes = mutableVectorOf<NodeState>()
private val scope = Scope()
private val precomposeMap = mutableMapOf<Any?, LayoutNode>()
private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet()
@@ -388,6 +394,20 @@
private var reusableCount = 0
private var precomposedCount = 0
+ init {
+ root.onDetach = {
+ // UiApplier.clear removes a single level of nodes, but dispatches detach recursively.
+ // SubcomposeLayout traverses children manually, so explicitly removing children from
+ // it here whenever root is detached.
+ // As parent detach happens before children, onDetach for children nodes will only be
+ // called once.
+ disposeCurrentNodes()
+ root.onAttach = {
+ throw IllegalStateException("Disposed subcompose root shouldn't be reattached")
+ }
+ }
+ }
+
fun subcompose(slotId: Any?, content: @Composable () -> Unit): List<Measurable> {
makeSureStateIsConsistent()
val layoutState = root.layoutState
@@ -402,7 +422,8 @@
precomposedCount--
precomposed
} else {
- takeNodeFromReusables(slotId) ?: createNodeAt(currentIndex)
+ takeNodeFromReusables(slotId)
+ ?: createNodeAt(currentIndex)
}
}
@@ -426,47 +447,65 @@
val nodeState = nodeToNodeState.getOrPut(node) {
NodeState(slotId, {})
}
+
val hasPendingChanges = nodeState.composition?.hasInvalidations ?: true
if (nodeState.content !== content || hasPendingChanges || nodeState.forceRecompose) {
- nodeState.content = content
- subcompose(node, nodeState)
+ subcompose(node, nodeState, content)
nodeState.forceRecompose = false
}
}
- private fun subcompose(node: LayoutNode, nodeState: NodeState) {
+ private fun subcompose(
+ node: LayoutNode,
+ nodeState: NodeState,
+ content: @Composable () -> Unit
+ ) {
Snapshot.withoutReadObservation {
- ignoreRemeasureRequests {
- val content = nodeState.content
- nodeState.composition = subcomposeInto(
- existing = nodeState.composition,
- container = node,
- parent = compositionContext ?: error("parent composition reference not set"),
- // Do not optimize this by passing nodeState.content directly; the additional
- // composable function call from the lambda expression affects the scope of
- // recomposition and recomposition of siblings.
- composable = {
- ReusableContentHost(nodeState.active, content)
- }
- )
+ if (nodeState.forceResetContent) {
+ require(nodeState.active) { "Node must be active to reset content" }
+ nodeState.active = false
+ subcomposeNode(node, nodeState)
+ nodeState.active = true
}
+
+ require(nodeState.active && !nodeState.forceResetContent) {
+ "Node must be active with content cleared for correct subcompose"
+ }
+
+ nodeState.content = content
+ subcomposeNode(node, nodeState)
}
}
- private fun subcomposeInto(
- existing: Composition?,
- container: LayoutNode,
- parent: CompositionContext,
- composable: @Composable () -> Unit
- ): Composition {
- return if (existing == null || existing.isDisposed) {
- createSubcomposition(container, parent)
- } else {
- existing
+ private fun subcomposeNode(
+ node: LayoutNode,
+ nodeState: NodeState
+ ) {
+ ignoreRemeasureRequests {
+ val content = nodeState.content
+ val existing = nodeState.composition
+ val parentContext = compositionContext ?: error("parent composition reference not set")
+
+ nodeState.composition =
+ if (existing == null || existing.isDisposed) {
+ createSubcomposition(node, parentContext)
+ } else {
+ existing
+ }
+ .apply {
+ setContent {
+ val active = nodeState.active
+
+ SideEffect {
+ if (!active) {
+ nodeState.forceResetContent = false
+ }
+ }
+
+ ReusableContentHost(active, content)
+ }
+ }
}
- .apply {
- setContent(composable)
- }
}
private fun getSlotIdAtIndex(index: Int): Any? {
@@ -474,29 +513,33 @@
return nodeToNodeState[node]!!.slotId
}
- fun disposeOrReuseStartingFromIndex(startIndex: Int) {
+ fun disposeOrReuseStartingFromIndex(startIndex: Int, retainAllSlots: Boolean = false) {
reusableCount = 0
val lastReusableIndex = root.foldedChildren.size - precomposedCount - 1
if (startIndex <= lastReusableIndex) {
// construct the set of available slot ids
reusableSlotIdsSet.clear()
- for (i in startIndex..lastReusableIndex) {
- reusableSlotIdsSet.add(getSlotIdAtIndex(i))
- }
+ if (!retainAllSlots) {
+ for (i in startIndex..lastReusableIndex) {
+ reusableSlotIdsSet.add(getSlotIdAtIndex(i))
+ }
- slotReusePolicy.getSlotsToRetain(reusableSlotIdsSet)
+ slotReusePolicy.getSlotsToRetain(reusableSlotIdsSet)
+ }
// iterating backwards so it is easier to remove items
var i = lastReusableIndex
while (i >= startIndex) {
val node = root.foldedChildren[i]
val nodeState = nodeToNodeState[node]!!
val slotId = nodeState.slotId
- if (reusableSlotIdsSet.contains(slotId)) {
+ if (retainAllSlots || reusableSlotIdsSet.contains(slotId)) {
node.measuredByParent = UsageByParent.NotUsed
reusableCount++
nodeState.active = false
+ nodeState.forceResetContent = true
} else {
ignoreRemeasureRequests {
+ slotIdToNode.remove(slotId)
nodeToNodeState.remove(node)
nodeState.composition?.dispose()
root.removeAt(i, 1)
@@ -675,6 +718,19 @@
}
}
+ private fun scheduleNodeDispose(node: LayoutNode) {
+ val nodeState = nodeToNodeState.remove(node) ?: return
+ precomposeMap.remove(nodeState.slotId)?.let {
+ precomposedCount--
+ }
+ slotIdToNode.remove(nodeState.slotId)
+ if (!disposeOnDetach) {
+ detachedNodes += nodeState
+ } else {
+ nodeState.composition?.dispose()
+ }
+ }
+
fun forceRecomposeChildren() {
nodeToNodeState.forEach { (_, nodeState) ->
nodeState.forceRecompose = true
@@ -699,18 +755,56 @@
private inline fun ignoreRemeasureRequests(block: () -> Unit) =
root.ignoreRemeasureRequests(block)
- fun disposeCurrentNodes() {
- root.ignoreRemeasureRequests {
- nodeToNodeState.values.forEach {
- it.composition?.dispose()
- }
- root.removeAll()
+ fun keepDetachedNodes() {
+ disposeOnDetach = false
+ }
+
+ fun disposeOrReuseChildren() {
+ reuseCurrentNodes()
+ disposeDetachedNodes()
+
+ makeSureStateIsConsistent()
+
+ disposeOnDetach = true
+ }
+
+ private fun reuseCurrentNodes() {
+ precomposedCount = 0
+ precomposeMap.clear()
+ disposeOrReuseStartingFromIndex(0, retainAllSlots = true)
+ }
+
+ private fun disposeDetachedNodes() {
+ detachedNodes.forEach {
+ it.composition?.dispose()
}
+ detachedNodes.clear()
+ }
+
+ private fun disposeCurrentNodes() {
+ if (!disposeOnDetach) {
+ // Node is detached before DisposableEffect.onDispose, make sure we remove
+ // composition there to save the state
+ detachedNodes.addAll(nodeToNodeState.values)
+ } else {
+ // Effect already disposed this state, dispose all compositions
+ root.ignoreRemeasureRequests {
+ nodeToNodeState.values.forEach {
+ it.composition?.dispose()
+ }
+ }
+ }
+
nodeToNodeState.clear()
slotIdToNode.clear()
precomposedCount = 0
reusableCount = 0
precomposeMap.clear()
+
+ root.ignoreRemeasureRequests {
+ root.removeAll()
+ }
+
makeSureStateIsConsistent()
}
@@ -720,7 +814,8 @@
var composition: Composition? = null
) {
var forceRecompose = false
- var active by mutableStateOf(true)
+ var forceResetContent = false
+ var active: Boolean by mutableStateOf(true)
}
private inner class Scope : SubcomposeMeasureScope {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 5dcc5f8..a2f0c22 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -300,6 +300,10 @@
onChildRemoved(_foldedChildren[i])
}
_foldedChildren.clear()
+
+ if (DebugChanges) {
+ println("Removed all children from $this")
+ }
}
private fun onChildRemoved(child: LayoutNode) {
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
index c93b13f..c072b44 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/ConstraintLayoutTest.kt
@@ -136,12 +136,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (displaySize.width / 2),
+ (displaySize.width / 2f).roundToInt(),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (displaySize.width / 2 / 2),
+ (displaySize.width / 2f / 2f).roundToInt(),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -197,12 +197,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (displaySize.width / 2),
+ (displaySize.width / 2f).roundToInt(),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (displaySize.width / 2 / 2),
+ (displaySize.width / 2f / 2f).roundToInt(),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -258,12 +258,12 @@
rule.runOnIdle {
// The aspect ratio could not wrap and it is wrap suggested, so it respects constraints.
assertEquals(
- (displaySize.width / 2),
+ (displaySize.width / 2f).roundToInt(),
aspectRatioBoxSize.value!!.width
)
// Aspect ratio is preserved.
assertEquals(
- (displaySize.width / 2 / 2),
+ (displaySize.width / 2f / 2f).roundToInt(),
aspectRatioBoxSize.value!!.height
)
// Divider has fixed width 1.dp in constraint set.
@@ -424,7 +424,7 @@
val boxSize = 100
val offset = 150
- val position = Array(3) { Ref<Offset>() }
+ val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
rule.setContent {
ConstraintLayout(
@@ -441,7 +441,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[0].value = it.positionInRoot()
+ position[0] = it.positionInRoot().round()
}
)
val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
@@ -453,7 +453,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[1].value = it.positionInRoot()
+ position[1] = it.positionInRoot().round()
}
)
Box(
@@ -464,7 +464,7 @@
}
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[2].value = it.positionInRoot()
+ position[2] = it.positionInRoot().round()
}
)
}
@@ -476,24 +476,24 @@
rule.runOnIdle {
assertEquals(
Offset(
- ((displayWidth - boxSize) / 2).toFloat(),
- ((displayHeight - boxSize) / 2).toFloat()
- ),
- position[0].value
+ ((displayWidth - boxSize) / 2f),
+ ((displayHeight - boxSize) / 2f)
+ ).round(),
+ position[0]
)
assertEquals(
Offset(
- (displayWidth / 2 + offset).toFloat(),
- ((displayHeight - boxSize) / 2 - boxSize).toFloat()
- ),
- position[1].value
+ (displayWidth / 2f + offset),
+ ((displayHeight - boxSize) / 2f - boxSize)
+ ).round(),
+ position[1]
)
assertEquals(
- Offset(
- offset.toFloat(),
- (displayHeight - boxSize - offset).toFloat()
+ IntOffset(
+ offset,
+ (displayHeight - boxSize - offset)
),
- position[2].value
+ position[2]
)
}
}
@@ -504,7 +504,7 @@
val boxSize = 100
val offset = 150
- val position = Array(3) { Ref<Offset>() }
+ val position: Array<IntOffset> = Array(3) { IntOffset.Zero }
rule.setContent {
ConstraintLayout(
@@ -540,7 +540,7 @@
.layoutId("box$i")
.size(boxSize.toDp(), boxSize.toDp())
.onGloballyPositioned {
- position[i].value = it.positionInRoot()
+ position[i] = it.positionInRoot().round()
}
)
}
@@ -555,22 +555,22 @@
Offset(
(displayWidth - boxSize) / 2f,
(displayHeight - boxSize) / 2f
- ),
- position[0].value
+ ).round(),
+ position[0]
)
assertEquals(
Offset(
(displayWidth / 2f + offset),
- ((displayHeight - boxSize) / 2 - boxSize).toFloat()
- ),
- position[1].value
+ ((displayHeight - boxSize) / 2f - boxSize)
+ ).round(),
+ position[1]
)
assertEquals(
- Offset(
- offset.toFloat(),
- (displayHeight - boxSize - offset).toFloat()
+ IntOffset(
+ offset,
+ (displayHeight - boxSize - offset)
),
- position[2].value
+ position[2]
)
}
}
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 52d84ae..f85f282 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -44,6 +44,7 @@
android {
defaultConfig {
+ minSdkVersion 31
multiDexEnabled = true
}
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 85d148ce..d79149c 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -55,6 +55,7 @@
android {
namespace "androidx.core.uwb"
defaultConfig {
+ minSdkVersion 31
multiDexEnabled = true
}
}
diff --git a/core/uwb/uwb/src/main/AndroidManifest.xml b/core/uwb/uwb/src/main/AndroidManifest.xml
index ef5bd56..fd303e0 100644
--- a/core/uwb/uwb/src/main/AndroidManifest.xml
+++ b/core/uwb/uwb/src/main/AndroidManifest.xml
@@ -15,5 +15,7 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
-
-</manifest>
\ No newline at end of file
+ <queries>
+ <package android:name="androidx.core.uwb.backend" />
+ </queries>
+</manifest>
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
new file mode 100644
index 0000000..b946a89
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IRangingSessionCallback.java
@@ -0,0 +1,291 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingSessionCallback
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "CallbackMethodName"})
+public interface IRangingSessionCallback extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IRangingSessionCallback. */
+ public static class Default implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ @Override public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException
+ {
+ }
+ @Override public void onRangingResult(@NonNull UwbDevice device,
+ @NonNull RangingPosition position) throws android.os.RemoteException
+ {
+ }
+ @Override public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IRangingSessionCallback interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IRangingSessionCallback asInterface(
+ @Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IRangingSessionCallback))) {
+ return ((androidx.core.uwb.backend.IRangingSessionCallback)iin);
+ }
+ return new androidx.core.uwb.backend.IRangingSessionCallback.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_onRangingInitialized:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ this.onRangingInitialized(_arg0);
+ break;
+ }
+ case TRANSACTION_onRangingResult:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ androidx.core.uwb.backend.RangingPosition _arg1;
+ _arg1 = data.readTypedObject(androidx.core.uwb.backend.RangingPosition.CREATOR);
+ this.onRangingResult(_arg0, _arg1);
+ break;
+ }
+ case TRANSACTION_onRangingSuspended:
+ {
+ androidx.core.uwb.backend.UwbDevice _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ int _arg1;
+ _arg1 = data.readInt();
+ this.onRangingSuspended(_arg0, _arg1);
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IRangingSessionCallback
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public void onRangingInitialized(@NonNull androidx.core.uwb.backend.UwbDevice device) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingInitialized, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingInitialized is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override public void onRangingResult(@NonNull androidx.core.uwb.backend.UwbDevice device, @NonNull androidx.core.uwb.backend.RangingPosition position) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ _data.writeTypedObject(position, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingResult, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingResult is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override public void onRangingSuspended(@NonNull androidx.core.uwb.backend.UwbDevice device, int reason) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(device, 0);
+ _data.writeInt(reason);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_onRangingSuspended, _data, null, android.os.IBinder.FLAG_ONEWAY);
+ if (!_status) {
+ throw new android.os.RemoteException("Method onRangingSuspended is unimplemented.");
+ }
+ }
+ finally {
+ _data.recycle();
+ }
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ @Override
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_onRangingInitialized = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_onRangingResult = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_onRangingSuspended = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR = "androidx"
+ + ".core.uwb.backend.IRangingSessionCallback";
+ /** Reasons for suspend */
+ public static final int UNKNOWN = 0;
+ public static final int WRONG_PARAMETERS = 1;
+ public static final int FAILED_TO_START = 2;
+ public static final int STOPPED_BY_PEER = 3;
+ public static final int STOP_RANGING_CALLED = 4;
+ @SuppressLint("MinMaxConstant")
+ public static final int MAX_RANGING_ROUND_RETRY_REACHED = 5;
+ public void onRangingInitialized(@NonNull UwbDevice device) throws android.os.RemoteException;
+ public void onRangingResult(@NonNull UwbDevice device, @NonNull RangingPosition position) throws android.os.RemoteException;
+ public void onRangingSuspended(@NonNull UwbDevice device, int reason) throws android.os.RemoteException;
+ @SuppressLint("CallbackMethodName")
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ @SuppressLint("CallbackMethodName")
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
new file mode 100644
index 0000000..5008f91
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwb.java
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+ /**
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal"})
+public interface IUwb extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IUwb. */
+ public static class Default implements androidx.core.uwb.backend.IUwb
+ {
+ @NonNull
+ @Override
+ public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+ {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwb
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IUwb interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IUwb asInterface(@Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwb))) {
+ return ((androidx.core.uwb.backend.IUwb)iin);
+ }
+ return new androidx.core.uwb.backend.IUwb.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_getControleeClient:
+ {
+ androidx.core.uwb.backend.IUwbClient _result = this.getControleeClient();
+ reply.writeNoException();
+ reply.writeStrongInterface(_result);
+ break;
+ }
+ case TRANSACTION_getControllerClient:
+ {
+ androidx.core.uwb.backend.IUwbClient _result = this.getControllerClient();
+ reply.writeNoException();
+ reply.writeStrongInterface(_result);
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IUwb
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.IUwbClient _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getControleeClient, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getControleeClient is unimplemented.");
+ }
+ _reply.readException();
+ _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.IUwbClient _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getControllerClient, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getControllerClient is unimplemented.");
+ }
+ _reply.readException();
+ _result = androidx.core.uwb.backend.IUwbClient.Stub.asInterface(_reply.readStrongBinder());
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @Override
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_getControleeClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_getControllerClient = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR =
+ "androidx.core.uwb.backend.IUwb";
+
+ @NonNull
+ public androidx.core.uwb.backend.IUwbClient getControleeClient() throws android.os.RemoteException;
+
+ @NonNull
+ public androidx.core.uwb.backend.IUwbClient getControllerClient() throws android.os.RemoteException;
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
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
new file mode 100644
index 0000000..d19d1eb
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/IUwbClient.java
@@ -0,0 +1,469 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.IBinder;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbClient
+ *
+ * @hide
+ */
+@SuppressLint({"MutableBareField", "ParcelNotFinal", "ExecutorRegistration"})
+public interface IUwbClient extends android.os.IInterface
+{
+ /**
+ * The version of this interface that the caller is built against.
+ * This might be different from what {@link #getInterfaceVersion()
+ * getInterfaceVersion} returns as that is the version of the interface
+ * that the remote object is implementing.
+ */
+ public static final int VERSION = 1;
+ public static final String HASH = "notfrozen";
+ /** Default implementation for IUwbClient. */
+ public static class Default implements androidx.core.uwb.backend.IUwbClient
+ {
+ @Override public boolean isAvailable() throws android.os.RemoteException
+ {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Nullable
+ @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @Nullable
+ @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+ {
+ return null;
+ }
+ @SuppressLint("ExecutorRegistration")
+ @Override public void startRanging(@NonNull RangingParameters parameters,
+ @NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ }
+ @SuppressLint("ExecutorRegistration")
+ @Override public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ }
+ @Override public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+ {
+ }
+ @Override public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException
+ {
+ }
+ @Override
+ public int getInterfaceVersion() {
+ return 0;
+ }
+
+ @NonNull
+ @Override
+ public String getInterfaceHash() {
+ return "";
+ }
+ @Override
+ @SuppressLint("MissingNullability")
+ public android.os.IBinder asBinder() {
+ return null;
+ }
+ }
+ /** Local-side IPC implementation stub class. */
+ @SuppressLint("RawAidl")
+ public static abstract class Stub extends android.os.Binder implements androidx.core.uwb.backend.IUwbClient
+ {
+ /** Construct the stub at attach it to the interface. */
+ public Stub()
+ {
+ this.attachInterface(this, DESCRIPTOR);
+ }
+ /**
+ * Cast an IBinder object into an androidx.core.uwb.backend.IUwbClient interface,
+ * generating a proxy if needed.
+ */
+ @Nullable
+ public static androidx.core.uwb.backend.IUwbClient asInterface(@Nullable IBinder obj)
+ {
+ if ((obj==null)) {
+ return null;
+ }
+ android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
+ if (((iin!=null)&&(iin instanceof androidx.core.uwb.backend.IUwbClient))) {
+ return ((androidx.core.uwb.backend.IUwbClient)iin);
+ }
+ return new androidx.core.uwb.backend.IUwbClient.Stub.Proxy(obj);
+ }
+ @SuppressLint("MissingNullability")
+ @Override public android.os.IBinder asBinder()
+ {
+ return this;
+ }
+ @Override public boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
+ int flags) throws android.os.RemoteException
+ {
+ java.lang.String descriptor = DESCRIPTOR;
+ if (code >= android.os.IBinder.FIRST_CALL_TRANSACTION && code <= android.os.IBinder.LAST_CALL_TRANSACTION) {
+ data.enforceInterface(descriptor);
+ }
+ switch (code)
+ {
+ case INTERFACE_TRANSACTION:
+ {
+ reply.writeString(descriptor);
+ return true;
+ }
+ case TRANSACTION_getInterfaceVersion:
+ {
+ reply.writeNoException();
+ reply.writeInt(getInterfaceVersion());
+ return true;
+ }
+ case TRANSACTION_getInterfaceHash:
+ {
+ reply.writeNoException();
+ reply.writeString(getInterfaceHash());
+ return true;
+ }
+ }
+ switch (code)
+ {
+ case TRANSACTION_isAvailable:
+ {
+ boolean _result = this.isAvailable();
+ reply.writeNoException();
+ reply.writeBoolean(_result);
+ break;
+ }
+ case TRANSACTION_getRangingCapabilities:
+ {
+ androidx.core.uwb.backend.RangingCapabilities _result = this.getRangingCapabilities();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getLocalAddress:
+ {
+ androidx.core.uwb.backend.UwbAddress _result = this.getLocalAddress();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_getComplexChannel:
+ {
+ androidx.core.uwb.backend.UwbComplexChannel _result = this.getComplexChannel();
+ reply.writeNoException();
+ reply.writeTypedObject(_result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+ break;
+ }
+ case TRANSACTION_startRanging:
+ {
+ androidx.core.uwb.backend.RangingParameters _arg0;
+ _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());
+ this.startRanging(_arg0, _arg1);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_stopRanging:
+ {
+ androidx.core.uwb.backend.IRangingSessionCallback _arg0;
+ _arg0 = androidx.core.uwb.backend.IRangingSessionCallback.Stub.asInterface(data.readStrongBinder());
+ this.stopRanging(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_addControlee:
+ {
+ androidx.core.uwb.backend.UwbAddress _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ this.addControlee(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ case TRANSACTION_removeControlee:
+ {
+ androidx.core.uwb.backend.UwbAddress _arg0;
+ _arg0 = data.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ this.removeControlee(_arg0);
+ reply.writeNoException();
+ break;
+ }
+ default:
+ {
+ return super.onTransact(code, data, reply, flags);
+ }
+ }
+ return true;
+ }
+ private static class Proxy implements androidx.core.uwb.backend.IUwbClient
+ {
+ private android.os.IBinder mRemote;
+ Proxy(android.os.IBinder remote)
+ {
+ mRemote = remote;
+ }
+ private int mCachedVersion = -1;
+ private String mCachedHash = "-1";
+ @Override public android.os.IBinder asBinder()
+ {
+ return mRemote;
+ }
+ @SuppressLint("UnusedMethod")
+ public java.lang.String getInterfaceDescriptor()
+ {
+ return DESCRIPTOR;
+ }
+ @Override public boolean isAvailable() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ boolean _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_isAvailable, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method isAvailable is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readBoolean();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @NonNull
+ @Override public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.RangingCapabilities _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getRangingCapabilities, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getRangingCapabilities is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.RangingCapabilities.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.UwbAddress _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getLocalAddress, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getLocalAddress is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ androidx.core.uwb.backend.UwbComplexChannel _result;
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getComplexChannel, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method getComplexChannel is unimplemented.");
+ }
+ _reply.readException();
+ _result = _reply.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ return _result;
+ }
+ @Override public void startRanging(@NonNull androidx.core.uwb.backend.RangingParameters parameters, @NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(parameters, 0);
+ _data.writeStrongInterface(callback);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_startRanging, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method startRanging is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void stopRanging(@NonNull androidx.core.uwb.backend.IRangingSessionCallback callback) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeStrongInterface(callback);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_stopRanging, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method stopRanging is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void addControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(address, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_addControlee, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method addControlee is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override public void removeControlee(@NonNull androidx.core.uwb.backend.UwbAddress address) throws android.os.RemoteException
+ {
+ android.os.Parcel _data = android.os.Parcel.obtain();
+ android.os.Parcel _reply = android.os.Parcel.obtain();
+ try {
+ _data.writeInterfaceToken(DESCRIPTOR);
+ _data.writeTypedObject(address, 0);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_removeControlee, _data, _reply, 0);
+ if (!_status) {
+ throw new android.os.RemoteException("Method removeControlee is unimplemented.");
+ }
+ _reply.readException();
+ }
+ finally {
+ _reply.recycle();
+ _data.recycle();
+ }
+ }
+ @Override
+ @SuppressLint("UnusedVariable")
+ public int getInterfaceVersion() throws android.os.RemoteException {
+ if (mCachedVersion == -1) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceVersion, data, reply, 0);
+ reply.readException();
+ mCachedVersion = reply.readInt();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedVersion;
+ }
+ @NonNull
+ @Override
+ @SuppressLint({"BanSynchronizedMethods", "UnusedVariable"})
+ public synchronized String getInterfaceHash() throws android.os.RemoteException {
+ if ("-1".equals(mCachedHash)) {
+ android.os.Parcel data = android.os.Parcel.obtain();
+ android.os.Parcel reply = android.os.Parcel.obtain();
+ try {
+ data.writeInterfaceToken(DESCRIPTOR);
+ boolean _status = mRemote.transact(Stub.TRANSACTION_getInterfaceHash, data, reply, 0);
+ reply.readException();
+ mCachedHash = reply.readString();
+ } finally {
+ reply.recycle();
+ data.recycle();
+ }
+ }
+ return mCachedHash;
+ }
+ }
+ static final int TRANSACTION_isAvailable = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
+ static final int TRANSACTION_getRangingCapabilities = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
+ static final int TRANSACTION_getLocalAddress = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
+ static final int TRANSACTION_getComplexChannel = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
+ static final int TRANSACTION_startRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
+ static final int TRANSACTION_stopRanging = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
+ static final int TRANSACTION_addControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
+ static final int TRANSACTION_removeControlee = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
+ static final int TRANSACTION_getInterfaceVersion = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777214);
+ static final int TRANSACTION_getInterfaceHash = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16777213);
+ }
+ public static final java.lang.String DESCRIPTOR =
+ "androidx.core.uwb.backend.IUwbClient";
+ public boolean isAvailable() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.RangingCapabilities getRangingCapabilities() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.UwbAddress getLocalAddress() throws android.os.RemoteException;
+
+ @Nullable
+ public androidx.core.uwb.backend.UwbComplexChannel getComplexChannel() throws android.os.RemoteException;
+ @SuppressLint("ExecutorRegistration")
+ public void startRanging(@NonNull RangingParameters parameters,
+ @NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+ @SuppressLint("ExecutorRegistration")
+ public void stopRanging(@NonNull IRangingSessionCallback callback) throws android.os.RemoteException;
+ public void addControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+ public void removeControlee(@NonNull UwbAddress address) throws android.os.RemoteException;
+ public int getInterfaceVersion() throws android.os.RemoteException;
+
+ @NonNull
+ public String getInterfaceHash() throws android.os.RemoteException;
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
new file mode 100644
index 0000000..6da98f1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingCapabilities.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingCapabilities
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingCapabilities implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public boolean supportsDistance = false;
+ @SuppressLint("MutableBareField")
+ public boolean supportsAzimuthalAngle = false;
+ @SuppressLint("MutableBareField")
+ public boolean supportsElevationAngle = false;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingCapabilities> CREATOR = new android.os.Parcelable.Creator<RangingCapabilities>() {
+ @Override
+ public RangingCapabilities createFromParcel(android.os.Parcel _aidl_source) {
+ RangingCapabilities _aidl_out = new RangingCapabilities();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingCapabilities[] newArray(int _aidl_size) {
+ return new RangingCapabilities[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeBoolean(supportsDistance);
+ _aidl_parcel.writeBoolean(supportsAzimuthalAngle);
+ _aidl_parcel.writeBoolean(supportsElevationAngle);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsDistance = _aidl_parcel.readBoolean();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsAzimuthalAngle = _aidl_parcel.readBoolean();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ supportsElevationAngle = _aidl_parcel.readBoolean();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
new file mode 100644
index 0000000..5b02460
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingMeasurement.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingMeasurement
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingMeasurement implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int confidence = 0;
+ @SuppressLint("MutableBareField")
+ public float value = 0.000000f;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingMeasurement> CREATOR = new android.os.Parcelable.Creator<RangingMeasurement>() {
+ @Override
+ public RangingMeasurement createFromParcel(android.os.Parcel _aidl_source) {
+ RangingMeasurement _aidl_out = new RangingMeasurement();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingMeasurement[] newArray(int _aidl_size) {
+ return new RangingMeasurement[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(confidence);
+ _aidl_parcel.writeFloat(value);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ confidence = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ value = _aidl_parcel.readFloat();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
new file mode 100644
index 0000000..966830c
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingParameters.java
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingParameters
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingParameters implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int uwbConfigId;
+ @SuppressLint("MutableBareField")
+ public int sessionId = 0;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public byte[] sessionKeyInfo;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.UwbComplexChannel complexChannel;
+ @Nullable
+ @SuppressLint({"MutableBareField", "NullableCollection"})
+ public java.util.List<androidx.core.uwb.backend.UwbDevice> peerDevices;
+ @SuppressLint("MutableBareField")
+ public int rangingUpdateRate = 0;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingParameters> CREATOR = new android.os.Parcelable.Creator<RangingParameters>() {
+ @Override
+ public RangingParameters createFromParcel(android.os.Parcel _aidl_source) {
+ RangingParameters _aidl_out = new RangingParameters();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingParameters[] newArray(int _aidl_size) {
+ return new RangingParameters[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(uwbConfigId);
+ _aidl_parcel.writeInt(sessionId);
+ _aidl_parcel.writeByteArray(sessionKeyInfo);
+ _aidl_parcel.writeTypedObject(complexChannel, _aidl_flag);
+ _aidl_parcel.writeTypedList(peerDevices);
+ _aidl_parcel.writeInt(rangingUpdateRate);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ uwbConfigId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionId = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ sessionKeyInfo = _aidl_parcel.createByteArray();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ complexChannel = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbComplexChannel.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ peerDevices = _aidl_parcel.createTypedArrayList(androidx.core.uwb.backend.UwbDevice.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ rangingUpdateRate = _aidl_parcel.readInt();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(complexChannel);
+ _mask |= describeContents(peerDevices);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof java.util.Collection) {
+ int _mask = 0;
+ for (Object o : (java.util.Collection) _v) {
+ _mask |= describeContents(o);
+ }
+ return _mask;
+ }
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
new file mode 100644
index 0000000..1173b81
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/RangingPosition.java
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.RangingPosition
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class RangingPosition implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement distance;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement azimuth;
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.RangingMeasurement elevation;
+ @SuppressLint("MutableBareField")
+ public long elapsedRealtimeNanos = 0L;
+ @NonNull
+ public static final android.os.Parcelable.Creator<RangingPosition> CREATOR = new android.os.Parcelable.Creator<RangingPosition>() {
+ @Override
+ public RangingPosition createFromParcel(android.os.Parcel _aidl_source) {
+ RangingPosition _aidl_out = new RangingPosition();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public RangingPosition[] newArray(int _aidl_size) {
+ return new RangingPosition[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedObject(distance, _aidl_flag);
+ _aidl_parcel.writeTypedObject(azimuth, _aidl_flag);
+ _aidl_parcel.writeTypedObject(elevation, _aidl_flag);
+ _aidl_parcel.writeLong(elapsedRealtimeNanos);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ distance = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ azimuth = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ elevation = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.RangingMeasurement.CREATOR);
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ elapsedRealtimeNanos = _aidl_parcel.readLong();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(distance);
+ _mask |= describeContents(azimuth);
+ _mask |= describeContents(elevation);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
new file mode 100644
index 0000000..e076cd1
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbAddress.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbAddress
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbAddress implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public byte[] address;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbAddress> CREATOR = new android.os.Parcelable.Creator<UwbAddress>() {
+ @Override
+ public UwbAddress createFromParcel(android.os.Parcel _aidl_source) {
+ UwbAddress _aidl_out = new UwbAddress();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbAddress[] newArray(int _aidl_size) {
+ return new UwbAddress[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeByteArray(address);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ address = _aidl_parcel.createByteArray();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
new file mode 100644
index 0000000..24d001a
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbComplexChannel.java
@@ -0,0 +1,82 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbComplexChannel
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbComplexChannel implements android.os.Parcelable
+{
+ @SuppressLint("MutableBareField")
+ public int channel = 0;
+ @SuppressLint("MutableBareField")
+ public int preambleIndex = 0;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbComplexChannel> CREATOR = new android.os.Parcelable.Creator<UwbComplexChannel>() {
+ @Override
+ public UwbComplexChannel createFromParcel(android.os.Parcel _aidl_source) {
+ UwbComplexChannel _aidl_out = new UwbComplexChannel();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbComplexChannel[] newArray(int _aidl_size) {
+ return new UwbComplexChannel[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeInt(channel);
+ _aidl_parcel.writeInt(preambleIndex);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ channel = _aidl_parcel.readInt();
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ preambleIndex = _aidl_parcel.readInt();
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
new file mode 100644
index 0000000..054aad7
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/backend/UwbDevice.java
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ *
+ * This file is auto-generated. DO NOT MODIFY.
+ */
+package androidx.core.uwb.backend;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/** Gms Reference: com.google.android.gms.nearby.uwb.UwbDevice
+ *
+ * @hide
+ */
+@SuppressLint({"ParcelNotFinal", "BanParcelableUsage"})
+public class UwbDevice implements android.os.Parcelable
+{
+ @Nullable
+ @SuppressLint("MutableBareField")
+ public androidx.core.uwb.backend.UwbAddress address;
+ @NonNull
+ public static final android.os.Parcelable.Creator<UwbDevice> CREATOR = new android.os.Parcelable.Creator<UwbDevice>() {
+ @Override
+ public UwbDevice createFromParcel(android.os.Parcel _aidl_source) {
+ UwbDevice _aidl_out = new UwbDevice();
+ _aidl_out.readFromParcel(_aidl_source);
+ return _aidl_out;
+ }
+ @Override
+ public UwbDevice[] newArray(int _aidl_size) {
+ return new UwbDevice[_aidl_size];
+ }
+ };
+ @Override public final void writeToParcel(@NonNull Parcel _aidl_parcel, int _aidl_flag)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.writeInt(0);
+ _aidl_parcel.writeTypedObject(address, _aidl_flag);
+ int _aidl_end_pos = _aidl_parcel.dataPosition();
+ _aidl_parcel.setDataPosition(_aidl_start_pos);
+ _aidl_parcel.writeInt(_aidl_end_pos - _aidl_start_pos);
+ _aidl_parcel.setDataPosition(_aidl_end_pos);
+ }
+ @SuppressLint("Finally")
+ public final void readFromParcel(@NonNull Parcel _aidl_parcel)
+ {
+ int _aidl_start_pos = _aidl_parcel.dataPosition();
+ int _aidl_parcelable_size = _aidl_parcel.readInt();
+ try {
+ if (_aidl_parcelable_size < 4) throw new android.os.BadParcelableException("Parcelable too small");;
+ if (_aidl_parcel.dataPosition() - _aidl_start_pos >= _aidl_parcelable_size) return;
+ address = _aidl_parcel.readTypedObject(androidx.core.uwb.backend.UwbAddress.CREATOR);
+ } finally {
+ if (_aidl_start_pos > (Integer.MAX_VALUE - _aidl_parcelable_size)) {
+ throw new android.os.BadParcelableException("Overflow in the size of parcelable");
+ }
+ _aidl_parcel.setDataPosition(_aidl_start_pos + _aidl_parcelable_size);
+ }
+ }
+ @Override
+ public int describeContents() {
+ int _mask = 0;
+ _mask |= describeContents(address);
+ return _mask;
+ }
+ private int describeContents(Object _v) {
+ if (_v == null) return 0;
+ if (_v instanceof android.os.Parcelable) {
+ return ((android.os.Parcelable) _v).describeContents();
+ }
+ return 0;
+ }
+}
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
new file mode 100644
index 0000000..fea81c2
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeAospImpl.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.uwb.impl
+
+import android.util.Log
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingMeasurement
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IRangingSessionCallback
+import androidx.core.uwb.backend.IUwbClient
+import androidx.core.uwb.backend.RangingPosition
+import androidx.core.uwb.backend.UwbComplexChannel
+import androidx.core.uwb.backend.UwbDevice
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+
+internal class UwbClientSessionScopeAospImpl(
+ private val uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress,
+) : UwbControleeSessionScope {
+ companion object {
+ private const val TAG = "UwbClientSessionScope"
+ }
+ private var sessionStarted = false
+
+ override fun prepareSession(parameters: RangingParameters) = callbackFlow {
+ if (sessionStarted) {
+ throw IllegalStateException("Ranging has already started. To initiate " +
+ "a new ranging session, create a new client session scope.")
+ }
+
+ val parametersBuilder1 = androidx.core.uwb.backend.RangingParameters()
+ parametersBuilder1.uwbConfigId = when (parameters.uwbConfigType) {
+ RangingParameters.UWB_CONFIG_ID_1 -> RangingParameters.UWB_CONFIG_ID_1
+ RangingParameters.UWB_CONFIG_ID_3 -> RangingParameters.UWB_CONFIG_ID_3
+ else -> throw IllegalArgumentException("The selected UWB Config Id is not a valid id.")
+ }
+ parametersBuilder1.rangingUpdateRate = when (parameters.updateRateType) {
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC ->
+ RangingParameters.RANGING_UPDATE_RATE_AUTOMATIC
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT ->
+ RangingParameters.RANGING_UPDATE_RATE_FREQUENT
+ RangingParameters.RANGING_UPDATE_RATE_INFREQUENT ->
+ RangingParameters.RANGING_UPDATE_RATE_INFREQUENT
+ else -> throw IllegalArgumentException(
+ "The selected ranging update rate is not a valid update rate.")
+ }
+ parametersBuilder1.sessionId = parameters.sessionId
+ parametersBuilder1.sessionKeyInfo = parameters.sessionKeyInfo
+ if (parameters.complexChannel != null) {
+ val channel = UwbComplexChannel()
+ channel.channel = parameters.complexChannel.channel
+ channel.preambleIndex = parameters.complexChannel.preambleIndex
+ parametersBuilder1.complexChannel = channel
+ }
+
+ val peerList = ArrayList<UwbDevice>()
+ for (peer in parameters.peerDevices) {
+ val device = UwbDevice()
+ val address = androidx.core.uwb.backend.UwbAddress()
+ address.address = peer.address.address
+ device.address = address
+ peerList.add(device)
+ }
+ parametersBuilder1.peerDevices = peerList
+ val callback =
+ object : IRangingSessionCallback.Stub() {
+ override fun onRangingInitialized(device: UwbDevice) {
+ Log.i(TAG, "Started UWB ranging.")
+ }
+
+ override fun onRangingResult(device: UwbDevice, position: RangingPosition) {
+ trySend(
+ RangingResult.RangingResultPosition(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!)),
+ androidx.core.uwb.RangingPosition(
+ position.distance?.let { RangingMeasurement(it.value) },
+ position.azimuth?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elevation?.let {
+ RangingMeasurement(it.value)
+ },
+ position.elapsedRealtimeNanos
+ )
+ )
+ )
+ }
+
+ override fun onRangingSuspended(device: UwbDevice, reason: Int) {
+ trySend(
+ RangingResult.RangingResultPeerDisconnected(
+ androidx.core.uwb.UwbDevice(UwbAddress(device.address?.address!!))
+ )
+ )
+ }
+
+ override fun getInterfaceVersion(): Int {
+ return 0
+ }
+
+ override fun getInterfaceHash(): String {
+ return ""
+ }
+ }
+
+ try {
+ uwbClient.startRanging(parametersBuilder1, callback)
+ sessionStarted = true
+ } catch (e: Exception) {
+ throw(e)
+ }
+
+ awaitClose {
+ CoroutineScope(Dispatchers.Main.immediate).launch {
+ try {
+ uwbClient.stopRanging(callback)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
new file mode 100644
index 0000000..e39bed3
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControleeSessionScopeAospImpl.kt
@@ -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.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbControleeSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControleeSessionScopeAospImpl(
+ uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress
+) : UwbControleeSessionScope {
+ private val uwbClientSessionScope =
+ UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+
+ override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+ return uwbClientSessionScope.prepareSession(parameters)
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
new file mode 100644
index 0000000..0630d41
--- /dev/null
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbControllerSessionScopeAospImpl.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.core.uwb.impl
+
+import androidx.core.uwb.RangingCapabilities
+import androidx.core.uwb.RangingParameters
+import androidx.core.uwb.RangingResult
+import androidx.core.uwb.UwbAddress
+import androidx.core.uwb.UwbComplexChannel
+import androidx.core.uwb.UwbControllerSessionScope
+import androidx.core.uwb.backend.IUwbClient
+import kotlinx.coroutines.flow.Flow
+
+internal class UwbControllerSessionScopeAospImpl(
+ private val uwbClient: IUwbClient,
+ override val rangingCapabilities: RangingCapabilities,
+ override val localAddress: UwbAddress,
+ override val uwbComplexChannel: UwbComplexChannel
+) : UwbControllerSessionScope {
+ private val uwbClientSessionScope =
+ UwbClientSessionScopeAospImpl(uwbClient, rangingCapabilities, localAddress)
+ override suspend fun addControlee(address: UwbAddress) {
+ val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+ uwbAddress.address = address.address
+ try {
+ uwbClient.addControlee(uwbAddress)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+
+ override suspend fun removeControlee(address: UwbAddress) {
+ val uwbAddress = androidx.core.uwb.backend.UwbAddress()
+ uwbAddress.address = address.address
+ try {
+ uwbClient.removeControlee(uwbAddress)
+ } catch (e: Exception) {
+ throw(e)
+ }
+ }
+
+ override fun prepareSession(parameters: RangingParameters): Flow<RangingResult> {
+ return uwbClientSessionScope.prepareSession(parameters)
+ }
+}
\ No newline at end of file
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
index bbf9cec..84297cf 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbManagerImpl.kt
@@ -16,7 +16,12 @@
package androidx.core.uwb.impl
+import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import android.util.Log
import androidx.core.uwb.RangingCapabilities
import androidx.core.uwb.UwbAddress
import androidx.core.uwb.UwbClientSessionScope
@@ -24,13 +29,35 @@
import androidx.core.uwb.UwbControleeSessionScope
import androidx.core.uwb.UwbControllerSessionScope
import androidx.core.uwb.UwbManager
+import androidx.core.uwb.backend.IUwb
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.nearby.Nearby
import kotlinx.coroutines.tasks.await
import androidx.core.uwb.helper.checkSystemFeature
import androidx.core.uwb.helper.handleApiException
+import com.google.android.gms.common.ConnectionResult
+import com.google.android.gms.common.GoogleApiAvailability
internal class UwbManagerImpl(private val context: Context) : UwbManager {
+ companion object {
+ const val TAG = "UwbMangerImpl"
+ var iUwb: IUwb? = null
+ }
+
+ init {
+ val connection = object : ServiceConnection {
+ override fun onServiceConnected(className: ComponentName, service: IBinder) {
+ iUwb = IUwb.Stub.asInterface(service)
+ Log.i(TAG, "iUwb service created successfully.")
+ }
+ override fun onServiceDisconnected(p0: ComponentName?) {
+ iUwb = null
+ }
+ }
+ val intent = Intent("androidx.core.uwb.backend.service")
+ intent.setPackage("androidx.core.uwb.backend")
+ context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
+ }
@Deprecated("Renamed to controleeSessionScope")
override suspend fun clientSessionScope(): UwbClientSessionScope {
return createClientSessionScope(false)
@@ -46,6 +73,14 @@
private suspend fun createClientSessionScope(isController: Boolean): UwbClientSessionScope {
checkSystemFeature(context)
+ val hasGmsCore = GoogleApiAvailability.getInstance()
+ .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS
+ return if (hasGmsCore) createGmsClientSessionScope(isController)
+ else createAospClientSessionScope(isController)
+ }
+
+ private suspend fun createGmsClientSessionScope(isController: Boolean): UwbClientSessionScope {
+ Log.i(TAG, "Creating Gms Client session scope")
val uwbClient = if (isController)
Nearby.getUwbControllerClient(context) else Nearby.getUwbControleeClient(context)
try {
@@ -77,4 +112,41 @@
"up-to-date with the service backend.")
}
}
+
+ private fun createAospClientSessionScope(isController: Boolean): UwbClientSessionScope {
+ Log.i(TAG, "Creating Aosp Client session scope")
+ val uwbClient = if (isController)
+ iUwb?.controllerClient else iUwb?.controleeClient
+ if (uwbClient == null) {
+ Log.e(TAG, "Failed to get UwbClient. AOSP backend is not available.")
+ }
+ try {
+ val aospLocalAddress = uwbClient!!.localAddress
+ val aospRangingCapabilities = uwbClient.rangingCapabilities
+ val localAddress = aospLocalAddress?.address?.let { UwbAddress(it) }
+ val rangingCapabilities = aospRangingCapabilities?.let {
+ RangingCapabilities(
+ it.supportsDistance,
+ it.supportsAzimuthalAngle,
+ it.supportsElevationAngle)
+ }
+ return if (isController) {
+ val uwbComplexChannel = uwbClient.complexChannel
+ UwbControllerSessionScopeAospImpl(
+ uwbClient,
+ rangingCapabilities!!,
+ localAddress!!,
+ UwbComplexChannel(uwbComplexChannel!!.channel, uwbComplexChannel.preambleIndex)
+ )
+ } else {
+ UwbControleeSessionScopeAospImpl(
+ uwbClient,
+ rangingCapabilities!!,
+ localAddress!!
+ )
+ }
+ } catch (e: Exception) {
+ throw e
+ }
+ }
}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/AndroidManifest.xml b/credentials/credentials-play-services-auth/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..d7b0e91
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<!--
+ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <activity
+ android:name="androidx.credentials.playservices.TestCredentialsActivity"
+ android:exported="false"
+ />
+ </application>
+</manifest>
\ No newline at end of file
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
new file mode 100644
index 0000000..3b04bca8
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerJavaTest.java
@@ -0,0 +1,248 @@
+/*
+ * 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;
+
+
+import static androidx.credentials.playservices.TestUtils.EXPECTED_LIFECYCLE_TAG;
+import static androidx.credentials.playservices.TestUtils.clearFragmentManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.credentials.CredentialManagerCallback;
+import androidx.credentials.GetCredentialRequest;
+import androidx.credentials.GetCredentialResponse;
+import androidx.credentials.GetPasswordOption;
+import androidx.credentials.exceptions.GetCredentialException;
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController;
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController;
+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.BeginSignInRequest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@RequiresApi(api = Build.VERSION_CODES.O)
+@SuppressWarnings("deprecation")
+public class CredentialProviderBeginSignInControllerJavaTest {
+
+ @Test
+ public void getInstance_createBrandNewFragment_constructSuccess() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+ android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+ clearFragmentManager(reusedFragmentManager);
+
+ assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+ EXPECTED_LIFECYCLE_TAG));
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+ CredentialProviderBeginSignInController actualBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+ assertThat(actualBeginSignInController).isNotNull();
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+ });
+
+ }
+
+ @Test
+ public void getInstance_createDifferentFragment_replaceWithNewFragmentSuccess() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+ android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+ clearFragmentManager(reusedFragmentManager);
+
+ assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+ EXPECTED_LIFECYCLE_TAG));
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+ CredentialProviderCreatePasswordController oldFragment =
+ CredentialProviderCreatePasswordController.getInstance(reusedFragmentManager);
+
+ assertThat(oldFragment).isNotNull();
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+ CredentialProviderBeginSignInController newFragment =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+ assertThat(newFragment).isNotNull();
+ assertThat(newFragment).isNotSameInstanceAs(oldFragment);
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+ });
+ }
+
+ @Test
+ public void getInstance_createFragment_replaceAttemptGivesBackSameFragmentSuccess() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+ android.app.FragmentManager reusedFragmentManager = activity.getFragmentManager();
+
+ clearFragmentManager(reusedFragmentManager);
+
+ assertThat(reusedFragmentManager.getFragments().get(0).getTag().equals(
+ EXPECTED_LIFECYCLE_TAG));
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+ CredentialProviderBeginSignInController expectedBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+ assertThat(expectedBeginSignInController).isNotNull();
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+
+ CredentialProviderBeginSignInController actualBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager);
+
+ assertThat(actualBeginSignInController).isNotNull();
+ assertThat(actualBeginSignInController)
+ .isSameInstanceAs(expectedBeginSignInController);
+ assertThat(reusedFragmentManager.getFragments().size()).isEqualTo(1);
+ });
+ }
+
+ @Test
+ public void invokePlayServices_success() {
+ // TODO(" Requires mocking inner Identity call. ")
+ }
+
+ @Test
+ public void convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ // 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
+ .getInstance(activity.getFragmentManager());
+ beginSignInController.callback = new CredentialManagerCallback<GetCredentialResponse,
+ GetCredentialException>() {
+ @Override
+ public void onResult(@NonNull GetCredentialResponse result) {
+
+ }
+ @Override
+ public void onError(@NonNull GetCredentialException e) {
+
+ }
+ };
+ 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)
+ ).getCredential();
+
+ assertThat(actualResponse.getType()).isEqualTo(expectedType);
+ assertThat(((PasswordCredential) actualResponse).getPassword())
+ .isEqualTo(expectedPassword);
+ assertThat(((PasswordCredential) actualResponse).getId()).isEqualTo(expectedId);
+ */
+ });
+ }
+
+ @Test
+ public void
+ convertRequestToPlayServices_setPasswordOptionRequestAndFalseAutoSelect_success() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+
+ BeginSignInRequest actualResponse =
+ CredentialProviderBeginSignInController
+ .getInstance(activity.getFragmentManager())
+ .convertRequestToPlayServices(new GetCredentialRequest(List.of(
+ new GetPasswordOption()
+ )));
+
+ assertThat(actualResponse.getPasswordRequestOptions().isSupported()).isTrue();
+ assertThat(actualResponse.isAutoSelectEnabled()).isFalse();
+ });
+ }
+
+ @Test
+ public void convertRequestToPlayServices_setPasswordOptionRequestAndTrueAutoSelect_success() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+
+ BeginSignInRequest actualResponse =
+ CredentialProviderBeginSignInController
+ .getInstance(activity.getFragmentManager())
+ .convertRequestToPlayServices(new GetCredentialRequest(List.of(
+ new GetPasswordOption()
+ ), true));
+
+ assertThat(actualResponse.getPasswordRequestOptions().isSupported()).isTrue();
+ assertThat(actualResponse.isAutoSelectEnabled()).isTrue();
+ });
+ }
+
+ @Test
+ public void convertRequestToPlayServices_nullRequest_throws() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+
+ assertThrows(
+ "null get credential request must throw exception",
+ NullPointerException.class,
+ () -> CredentialProviderBeginSignInController
+ .getInstance(activity.getFragmentManager())
+ .convertRequestToPlayServices(null)
+ );
+ });
+ }
+
+ @Test
+ public void convertResponseToCredentialManager_nullRequest_throws() {
+ ActivityScenario<TestCredentialsActivity> activityScenario =
+ ActivityScenario.launch(TestCredentialsActivity.class);
+ activityScenario.onActivity(activity -> {
+
+ assertThrows(
+ "null sign in credential response must throw exception",
+ NullPointerException.class,
+ () -> CredentialProviderBeginSignInController
+ .getInstance(activity.getFragmentManager())
+ .convertResponseToCredentialManager(null)
+ );
+ });
+ }
+}
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
new file mode 100644
index 0000000..4f5a8ce
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/CredentialProviderBeginSignInControllerTest.kt
@@ -0,0 +1,236 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.GetPasswordOption
+import androidx.credentials.exceptions.GetCredentialException
+import androidx.credentials.playservices.TestUtils.Companion.clearFragmentManager
+import androidx.credentials.playservices.controllers.BeginSignIn.CredentialProviderBeginSignInController
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@Suppress("deprecation")
+@RequiresApi(api = Build.VERSION_CODES.O)
+class CredentialProviderBeginSignInControllerTest {
+ @Test
+ fun getInstance_createBrandNewFragment_constructSuccess() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ activityScenario.onActivity { activity: TestCredentialsActivity ->
+ val reusedFragmentManager = activity.fragmentManager
+
+ clearFragmentManager(
+ reusedFragmentManager
+ )
+
+ assertThat(
+ reusedFragmentManager.fragments[0].tag ==
+ TestUtils.EXPECTED_LIFECYCLE_TAG
+ )
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+
+ val actualBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+ assertThat(actualBeginSignInController).isNotNull()
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun getInstance_createDifferentFragment_replaceWithNewFragmentSuccess() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ activityScenario.onActivity { activity: TestCredentialsActivity ->
+ val reusedFragmentManager = activity.fragmentManager
+
+ clearFragmentManager(
+ reusedFragmentManager
+ )
+
+ assertThat(
+ reusedFragmentManager.fragments[0].tag ==
+ TestUtils.EXPECTED_LIFECYCLE_TAG
+ )
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+
+ val oldFragment =
+ CredentialProviderCreatePasswordController.getInstance(reusedFragmentManager)
+
+ assertThat(oldFragment).isNotNull()
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+
+ val newFragment =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+ assertThat(newFragment).isNotNull()
+ assertThat(newFragment).isNotSameInstanceAs(oldFragment)
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun getInstance_createFragment_replaceAttemptGivesBackSameFragmentSuccess() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ activityScenario.onActivity { activity: TestCredentialsActivity ->
+ val reusedFragmentManager = activity.fragmentManager
+
+ clearFragmentManager(
+ reusedFragmentManager
+ )
+
+ assertThat(
+ reusedFragmentManager.fragments[0].tag ==
+ TestUtils.EXPECTED_LIFECYCLE_TAG
+ )
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+
+ val expectedBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+ assertThat(expectedBeginSignInController).isNotNull()
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+
+ val actualBeginSignInController =
+ CredentialProviderBeginSignInController.getInstance(reusedFragmentManager)
+
+ assertThat(actualBeginSignInController).isNotNull()
+ assertThat(actualBeginSignInController)
+ .isSameInstanceAs(expectedBeginSignInController)
+ assertThat(reusedFragmentManager.fragments.size)
+ .isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun invokePlayServices_success() {
+ // TODO(" Requires mocking inner Identity call. ")
+ }
+
+ @Test
+ fun convertResponseToCredentialManager_signInCredentialPasswordInput_success() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ // 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
+ .getInstance(activity.fragmentManager)
+ beginSignInController.callback =
+ object :
+ CredentialManagerCallback<GetCredentialResponse, GetCredentialException> {
+ override fun onResult(result: GetCredentialResponse) {}
+ override fun onError(e: GetCredentialException) {}
+ }
+ 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
+ )
+ ).credential
+
+ assertThat(actualResponse.type).isEqualTo(expectedType)
+ assertThat((actualResponse as PasswordCredential).password)
+ .isEqualTo(expectedPassword)
+ assertThat(actualResponse.id)
+ .isEqualTo(expectedId)
+ */
+ }
+ }
+
+ @Test
+ fun convertRequestToPlayServices_setPasswordOptionRequestAndFalseAutoSelect_success() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ activityScenario.onActivity { activity: TestCredentialsActivity ->
+
+ val actualResponse = CredentialProviderBeginSignInController
+ .getInstance(activity.fragmentManager)
+ .convertRequestToPlayServices(
+ GetCredentialRequest(
+ listOf(
+ GetPasswordOption()
+ )
+ )
+ )
+
+ assertThat(
+ actualResponse.passwordRequestOptions.isSupported
+ ).isTrue()
+ assertThat(actualResponse.isAutoSelectEnabled).isFalse()
+ }
+ }
+
+ @Test
+ fun convertRequestToPlayServices_setPasswordOptionRequestAndTrueAutoSelect_success() {
+ val activityScenario = ActivityScenario.launch(
+ TestCredentialsActivity::class.java
+ )
+ activityScenario.onActivity { activity: TestCredentialsActivity ->
+
+ val actualResponse = CredentialProviderBeginSignInController
+ .getInstance(activity.fragmentManager)
+ .convertRequestToPlayServices(
+ GetCredentialRequest(
+ listOf(
+ GetPasswordOption()
+ ), true
+ )
+ )
+
+ assertThat(
+ actualResponse.passwordRequestOptions.isSupported
+ ).isTrue()
+ assertThat(actualResponse.isAutoSelectEnabled).isTrue()
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt
new file mode 100644
index 0000000..d57a9b6
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestCredentialsActivity.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+import androidx.core.app.ComponentActivity
+
+/**
+ * This is a test activity used by the Robolectric Activity Scenario tests. It acts
+ * as a calling activity in our test cases.
+ */
+class TestCredentialsActivity : ComponentActivity()
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.kt
new file mode 100644
index 0000000..b27d22c
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/java/androidx/credentials/playservices/TestUtils.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.playservices
+
+import android.os.Build
+import android.util.Log
+import androidx.annotation.RequiresApi
+
+class TestUtils {
+ companion object {
+ @JvmStatic
+ @RequiresApi(api = Build.VERSION_CODES.O)
+ @Suppress("deprecation")
+ fun clearFragmentManager(fragmentManager: android.app.FragmentManager) {
+ fragmentManager.fragments.forEach { f ->
+ fragmentManager.beginTransaction().remove(f)
+ ?.commitAllowingStateLoss()
+ }
+ Log.i("Test", fragmentManager.fragments.toString())
+ // Within fragmentManager.fragments, even after removal of all, this exists by default:
+ // [ReportFragment{92dad5d #0 androidx.lifecycle.LifecycleDispatcher.report_fragment_tag}]
+ // It will only be removed after an actual fragment is added.
+ // This may be due to ActivityScenario simulations of Fragments and FragmentManagers.
+ }
+
+ const val EXPECTED_LIFECYCLE_TAG =
+ "androidx.lifecycle.LifecycleDispatcher.report_fragment_tag"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml b/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml
new file mode 100644
index 0000000..09cd014
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/androidTest/res/values/strings.xml
@@ -0,0 +1,17 @@
+<!--
+ 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.
+ -->
+
+<resources></resources>
\ No newline at end of file
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 e5e59bf..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")
@@ -116,7 +121,7 @@
cancellationSignal.setOnCancelListener {
// When this callback is notified, fragment manager may have fragments
fragmentManager.fragments.forEach { f ->
- f?.parentFragment?.fragmentManager?.beginTransaction()?.remove(f)
+ fragmentManager.beginTransaction().remove(f)
?.commitAllowingStateLoss()
}
}
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 1c9f3c1..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
@@ -20,14 +20,15 @@
import android.app.Activity
import android.content.Intent
import android.content.IntentSender
-import android.os.Bundle
+import android.os.Build
import android.util.Log
+import androidx.annotation.VisibleForTesting
import androidx.credentials.Credential
import androidx.credentials.CredentialManagerCallback
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
@@ -57,14 +58,16 @@
/**
* The callback object state, used in the protected handleResponse method.
*/
- private lateinit var callback: CredentialManagerCallback<GetCredentialResponse,
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ lateinit var callback: CredentialManagerCallback<GetCredentialResponse,
GetCredentialException>
/**
* The callback requires an executor to invoke it.
*/
- private lateinit var executor: Executor
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ lateinit var executor: Executor
- @SuppressLint("ClassVerificationFailure", "NewApi")
+ @SuppressLint("ClassVerificationFailure")
override fun invokePlayServices(
request: GetCredentialRequest,
callback: CredentialManagerCallback<GetCredentialResponse, GetCredentialException>,
@@ -77,15 +80,17 @@
.beginSignIn(convertedRequest)
.addOnSuccessListener { result: BeginSignInResult ->
try {
- startIntentSenderForResult(
- result.pendingIntent.intentSender,
- REQUEST_CODE_BEGIN_SIGN_IN,
- null, /* fillInIntent= */
- 0, /* flagsMask= */
- 0, /* flagsValue= */
- 0, /* extraFlags= */
- null /* options= */
- )
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ startIntentSenderForResult(
+ result.pendingIntent.intentSender,
+ REQUEST_CODE_BEGIN_SIGN_IN,
+ null, /* fillInIntent= */
+ 0, /* flagsMask= */
+ 0, /* flagsValue= */
+ 0, /* extraFlags= */
+ null /* options= */
+ )
+ }
} catch (e: IntentSender.SendIntentException) {
Log.e(TAG, "Couldn't start One Tap UI in beginSignIn: " +
e.localizedMessage
@@ -127,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
@@ -144,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()
}
@@ -154,15 +159,23 @@
)
}
return
+ } catch (e: GetCredentialException) {
+ executor.execute { ->
+ callback.onError(
+ e
+ )
+ }
}
}
- override fun convertRequestToPlayServices(request: GetCredentialRequest):
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public override fun convertRequestToPlayServices(request: GetCredentialRequest):
BeginSignInRequest {
return constructBeginSignInRequest(request)
}
- override fun convertResponseToCredentialManager(response: SignInCredential):
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public override fun convertResponseToCredentialManager(response: SignInCredential):
GetCredentialResponse {
var cred: Credential? = null
if (response.password != null) {
@@ -175,11 +188,7 @@
Log.i(TAG, "Credential returned but no google Id or password or passkey found")
}
if (cred == null) {
- executor.execute { callback.onError(
- GetCredentialUnknownException(
- "cred should not be null")
- ) }
- return GetCredentialResponse(Credential("ERROR", Bundle.EMPTY))
+ throw GetCredentialUnknownException("null credential found")
}
return GetCredentialResponse(cred)
}
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 be6ba81..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
@@ -76,7 +76,7 @@
.savePassword(convertedRequest)
.addOnSuccessListener { result: SavePasswordResult ->
try {
- if (Build.VERSION.SDK_INT >= 24) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
startIntentSenderForResult(
result.pendingIntent.intentSender,
REQUEST_CODE_GIS_SAVE_PASSWORD,
@@ -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/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
index 7bf3ba1..7ef9840 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CreatePublicKeyCredentialRequestPrivilegedFailureInputsJavaTest.java
@@ -23,7 +23,6 @@
import org.junit.runners.Parameterized;
import java.util.Arrays;
-import java.util.Collection;
/**
* Combines with {@link CreatePublicKeyCredentialRequestPrivilegedJavaTest} for full tests.
@@ -50,11 +49,11 @@
}
@Parameterized.Parameters
- public static Collection failureCases() {
+ public static Iterable<String[]> failureCases() {
// Allows checking improper formations for builder and normal construction.
// Handles both null and empty cases.
// For successful cases, see the non parameterized tests.
- return Arrays.asList(new Object[][] {
+ return Arrays.asList(new String[][] {
{ "{\"hi\":21}", "rp", "", null, "rp", "hash"},
{ "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
{ "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
@@ -84,6 +83,7 @@
@Test
public void constructor_nullInput_throwsNullPointerException() {
+ convertAPIIssueToProperNull();
assertThrows("If at least one arg null, should throw NullPointerException",
NullPointerException.class,
() -> new CreatePublicKeyCredentialRequestPrivileged(this.mNullRequestJson,
@@ -94,6 +94,7 @@
@Test
public void builder_build_nullInput_throwsNullPointerException() {
+ convertAPIIssueToProperNull();
assertThrows(
"If at least one arg null to builder, should throw NullPointerException",
NullPointerException.class,
@@ -101,4 +102,20 @@
mNullRp, mNullClientDataHash).build()
);
}
+
+ // Certain API levels have parameterized tests that automatically convert null to a string
+ // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
+ // workaround. Note this is *not* always the case but only for certain API levels (we have
+ // recorded 21, 22, and 23 as such levels).
+ private void convertAPIIssueToProperNull() {
+ if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
+ mNullRequestJson = null;
+ }
+ if (mNullRp != null && mNullRp.equals("null")) {
+ mNullRp = null;
+ }
+ if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
+ mNullClientDataHash = null;
+ }
+ }
}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
index 9984393..be01f1c 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/GetPublicKeyCredentialOptionPrivilegedFailureInputsJavaTest.java
@@ -23,7 +23,6 @@
import org.junit.runners.Parameterized;
import java.util.Arrays;
-import java.util.Collection;
/**
* Combines with {@link GetPublicKeyCredentialOptionPrivilegedJavaTest} for full tests.
@@ -50,11 +49,11 @@
}
@Parameterized.Parameters
- public static Collection failureCases() {
+ public static Iterable<String[]> failureCases() {
// Allows checking improper formations for builder and normal construction.
// Handles both null and empty cases.
// For successful cases, see the non parameterized privileged tests.
- return Arrays.asList(new Object[][] {
+ return Arrays.asList(new String[][] {
{ "{\"hi\":21}", "rp", "", null, "rp", "hash"},
{ "", "rp", "clientDataHash", "{\"hi\":21}", null, "hash"},
{ "{\"hi\":21}", "", "clientDataHash", "{\"hi\":21}", "rp", null}
@@ -84,6 +83,7 @@
@Test
public void constructor_nullInput_throwsNullPointerException() {
+ convertAPIIssueToProperNull();
assertThrows(
"If at least one arg null, should throw NullPointerException",
NullPointerException.class,
@@ -95,6 +95,7 @@
@Test
public void builder_build_nullInput_throwsNullPointerException() {
+ convertAPIIssueToProperNull();
assertThrows(
"If at least one arg null to builder, should throw NullPointerException",
NullPointerException.class,
@@ -102,4 +103,20 @@
mNullRp, mNullClientDataHash).build()
);
}
+
+ // Certain API levels have parameterized tests that automatically convert null to a string
+ // 'null' causing test failures. Until Parameterized tests fixes this bug, this is the
+ // workaround. Note this is *not* always the case but only for certain API levels (we have
+ // recorded 21, 22, and 23 as such levels).
+ private void convertAPIIssueToProperNull() {
+ if (mNullRequestJson != null && mNullRequestJson.equals("null")) {
+ mNullRequestJson = null;
+ }
+ if (mNullRp != null && mNullRp.equals("null")) {
+ mNullRp = null;
+ }
+ if (mNullClientDataHash != null && mNullClientDataHash.equals("null")) {
+ mNullClientDataHash = null;
+ }
+ }
}
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/CreateCredentialCanceledExceptionTest.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/CreateCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
index 903bc23..3ff4aa3 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCancellationExceptionTest.kt
@@ -24,22 +24,22 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class CreateCredentialCanceledExceptionTest {
- @Test(expected = CreateCredentialCanceledException::class)
+class CreateCredentialCancellationExceptionTest {
+ @Test(expected = CreateCredentialCancellationException::class)
fun construct_inputNonEmpty_success() {
- throw CreateCredentialCanceledException("msg")
+ throw CreateCredentialCancellationException("msg")
}
- @Test(expected = CreateCredentialCanceledException::class)
+ @Test(expected = CreateCredentialCancellationException::class)
fun construct_errorMessageNull_success() {
- throw CreateCredentialCanceledException(null)
+ throw CreateCredentialCancellationException(null)
}
@Test
fun getter_type_success() {
- val exception = CreateCredentialCanceledException("msg")
- val expectedType = CreateCredentialCanceledException
- .TYPE_CREATE_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/GetCredentialCanceledExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
deleted file mode 100644
index 61e7c3c..0000000
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCanceledExceptionTest.kt
+++ /dev/null
@@ -1,44 +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 GetCredentialCanceledExceptionTest {
- @Test(expected = GetCredentialCanceledException::class)
- fun construct_inputNonEmpty_success() {
- throw GetCredentialCanceledException("msg")
- }
-
- @Test(expected = GetCredentialCanceledException::class)
- fun construct_errorMessageNull_success() {
- throw GetCredentialCanceledException(null)
- }
-
- @Test
- fun getter_type_success() {
- val exception = GetCredentialCanceledException("msg")
- val expectedType = GetCredentialCanceledException.TYPE_GET_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/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/CreateCredentialCanceledExceptionTest.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/CreateCredentialCanceledExceptionTest.kt
rename to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
index 903bc23..bbc2d3e 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialCancellationExceptionTest.kt
@@ -24,22 +24,22 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class CreateCredentialCanceledExceptionTest {
- @Test(expected = CreateCredentialCanceledException::class)
+class GetCredentialCancellationExceptionTest {
+ @Test(expected = GetCredentialCancellationException::class)
fun construct_inputNonEmpty_success() {
- throw CreateCredentialCanceledException("msg")
+ throw GetCredentialCancellationException("msg")
}
- @Test(expected = CreateCredentialCanceledException::class)
+ @Test(expected = GetCredentialCancellationException::class)
fun construct_errorMessageNull_success() {
- throw CreateCredentialCanceledException(null)
+ throw GetCredentialCancellationException(null)
}
@Test
fun getter_type_success() {
- val exception = CreateCredentialCanceledException("msg")
- val expectedType = CreateCredentialCanceledException
- .TYPE_CREATE_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/CreateCredentialCanceledExceptionTest.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/CreateCredentialCanceledExceptionTest.kt
copy to credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/createpublickeycredential/CreatePublicKeyCredentialAbortExceptionTest.kt
index 903bc23..123dc94 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialCanceledExceptionTest.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,22 +25,24 @@
@RunWith(AndroidJUnit4::class)
@SmallTest
-class CreateCredentialCanceledExceptionTest {
- @Test(expected = CreateCredentialCanceledException::class)
+class CreatePublicKeyCredentialAbortExceptionTest {
+
+ @Test(expected = CreatePublicKeyCredentialAbortException::class)
fun construct_inputNonEmpty_success() {
- throw CreateCredentialCanceledException("msg")
+ throw CreatePublicKeyCredentialAbortException("msg")
}
- @Test(expected = CreateCredentialCanceledException::class)
+ @Test(expected = CreatePublicKeyCredentialAbortException::class)
fun construct_errorMessageNull_success() {
- throw CreateCredentialCanceledException(null)
+ throw CreatePublicKeyCredentialAbortException(null)
}
@Test
fun getter_type_success() {
- val exception = CreateCredentialCanceledException("msg")
- val expectedType = CreateCredentialCanceledException
- .TYPE_CREATE_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/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index add01af..1fa5322 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -792,3 +792,7 @@
xcframework successfully .*
# iOS benchmark invocation
/usr/bin/xcodebuild
+# > Configure project :internal-testutils-ktx
+WARNING:The option setting 'android\.r8\.maxWorkers=[0-9]+' is experimental\.
+# Building XCFrameworks (b/260140834)
+.*xcodebuild.*
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/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index dd40d3a..bcc3091 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -33,6 +33,8 @@
implementation 'androidx.tracing:tracing-ktx:1.0.0'
implementation 'androidx.test.ext:junit-ktx:1.1.3'
+ androidTestImplementation(libs.bundles.espressoContrib, excludes.espresso)
+ androidTestImplementation(libs.espressoCore, excludes.espresso)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
index 8a8ebeb..baf6c45 100644
--- a/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
+++ b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
@@ -17,11 +17,24 @@
package androidx.emoji2.emojipicker.samples
import android.os.Bundle
+import android.util.Log
import androidx.appcompat.app.AppCompatActivity
+import androidx.emoji2.emojipicker.EmojiPickerView
+import androidx.emoji2.emojipicker.EmojiViewItem
class MainActivity : AppCompatActivity() {
+ companion object {
+ internal const val LOG_TAG = "SAMPLE_APP"
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
+
+ val view = findViewById<EmojiPickerView>(R.id.emoji_picker)
+ view.setOnEmojiPickedListener { emoji: EmojiViewItem ->
+ Log.d(LOG_TAG, String.format("emoji: %s", emoji.emoji))
+ Log.d(LOG_TAG, String.format("variants: %s", emoji.variants.toString()))
+ }
}
}
\ No newline at end of file
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/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
index 5f967ca..5ab8cb5 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import androidx.emoji2.emojipicker.utils.FileCache
import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
@@ -69,6 +70,7 @@
}
@Test
+ @SdkSuppress(minSdkVersion = 24)
fun testGetEmojiVariantsLookup_loaded() = runBlocking {
// delete cache and load again
FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
@@ -80,4 +82,19 @@
// 😀 has no variant
assertFalse(result.containsKey("\uD83D\uDE00"))
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 24)
+ fun testGetPrimaryEmojiLookup_loaded() = runBlocking {
+ // delete cache and load again
+ FileCache.getInstance(context).emojiPickerCacheDir.deleteRecursively()
+ BundledEmojiListLoader.load(context)
+ val result = BundledEmojiListLoader.getPrimaryEmojiLookup()
+
+ // 👃 has variants (👃,👃,👃🏻,👃🏼,👃🏽,👃🏾,👃🏿)
+ assertTrue(result["\uD83D\uDC43\uD83C\uDFFD"]!! == "\uD83D\uDC43")
+ assertTrue(result["\uD83D\uDC43\uD83C\uDFFF"]!! == "\uD83D\uDC43")
+ // 😀 has no variant
+ assertFalse(result.containsKey("\uD83D\uDE00"))
+ }
}
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 a63f725..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
@@ -16,15 +16,24 @@
package androidx.emoji2.emojipicker
+import androidx.emoji2.emojipicker.R as EmojiPickerViewR
import android.app.Activity
import android.content.Context
import android.os.Bundle
+import android.view.View
+import android.view.View.GONE
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.emoji2.emojipicker.test.R
+import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import org.hamcrest.Description
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
@@ -61,4 +70,63 @@
assertEquals(mEmojiPickerView.emojiGridColumns, 10)
}
}
+
+ @Test
+ fun testCustomEmojiPickerView_noVariant() {
+ mActivityTestRule.scenario.onActivity {
+ val targetView = findViewByEmoji(
+ it.findViewById(R.id.emojiPickerTest),
+ "\uD83D\uDE00"
+ )
+ // No variant indicator
+ assertEquals(
+ (targetView.parent as FrameLayout).findViewById<ImageView>(
+ EmojiPickerViewR.id.variant_availability_indicator
+ ).visibility,
+ GONE
+ )
+ // Not long-clickable
+ assertEquals(targetView.isLongClickable, false)
+ }
+ }
+
+ private fun findViewByEmoji(root: View, emoji: String) =
+ mutableListOf<View>().apply {
+ findViewsById(
+ root,
+ EmojiPickerViewR.id.emoji_view, this
+ )
+ }.first { (it as EmojiView).emoji == emoji }
+
+ private fun findViewsById(root: View, id: Int, output: MutableList<View>) {
+ if (root !is ViewGroup) {
+ return
+ }
+ for (i in 0 until root.childCount) {
+ root.getChildAt(i).apply {
+ if (this.id == id) {
+ output.add(this)
+ }
+ }.also {
+ findViewsById(it, id, output)
+ }
+ }
+ }
+
+ private fun createEmojiViewHolderMatcher(emoji: String) =
+ object :
+ BoundedMatcher<RecyclerView.ViewHolder, EmojiViewHolder>(EmojiViewHolder::class.java) {
+ override fun describeTo(description: Description) {}
+ override fun matchesSafely(item: EmojiViewHolder) =
+ (item.itemView as FrameLayout)
+ .findViewById<EmojiView>(EmojiPickerViewR.id.emoji_view)
+ .emoji == emoji
+ }
+
+ private fun createEmojiViewMatcher(emoji: String) =
+ object :
+ BoundedMatcher<View, EmojiView>(EmojiView::class.java) {
+ override fun describeTo(description: Description) {}
+ override fun matchesSafely(item: EmojiView) = item.emoji == emoji
+ }
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 57136d4..ed65d91 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -39,6 +39,7 @@
internal object BundledEmojiListLoader {
private var categorizedEmojiData: List<EmojiDataCategory>? = null
private var emojiVariantsLookup: Map<String, List<String>>? = null
+ private var primaryEmojiLookup: Map<String, String>? = null
private var deferred: List<Deferred<EmojiDataCategory>>? = null
@@ -67,6 +68,14 @@
.associate { it.emoji to it.variants }
.also { emojiVariantsLookup = it }
+ internal suspend fun getPrimaryEmojiLookup() =
+ primaryEmojiLookup ?: getCategorizedEmojiData()
+ .flatMap { it.emojiDataList }
+ .filter { it.variants.isNotEmpty() }
+ .flatMap { it.variants.associateWith { _ -> it.emoji }.entries }
+ .associate { it.toPair() }
+ .also { primaryEmojiLookup = it }
+
private suspend fun loadEmojiAsync(
ta: TypedArray,
categoryNames: Array<String>,
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt
deleted file mode 100644
index 2db67d7..0000000
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt
+++ /dev/null
@@ -1,50 +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
-
-/**
- * Separator for each category.
- *
- *
- * CategorySeparatorViewData: A 0-width `Space` at the beginning of each category. The
- * `Space` works an anchor (prevention from unexpected scrolling) when the contents of the
- * RecyclerView is updated.
- */
-internal class CategorySeparatorViewData
-/**
- * Instantiates a CategorySeparatorViewData.
- *
- * @param categoryIndex Used to compute the id.
- * @param idInCategory Used to compute the id.
- * @param categoryName The category name showing in the text view, e.g. "CUSTOM EMOJIS". If empty,
- * will look up the corresponding category name based on `categoryIndex`
- * in [EmojiPickerBodyAdapter.onBindViewHolder]
- */(
- categoryIndex: Int,
- idInCategory: Int,
- /** The name of this category. */
- val categoryName: String
-) :
- ItemViewData(calculateId(TYPE, categoryIndex, /* idInCategory= */idInCategory)) {
-
- override val type: Int
- get() = TYPE
-
- companion object {
- val TYPE = CategorySeparatorViewData::class.java.name.hashCode()
- }
-}
\ No newline at end of file
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 ba76f24..d16de67 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
@@ -17,150 +17,99 @@
package androidx.emoji2.emojipicker
import android.content.Context
-import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
-import androidx.annotation.IntRange
+import androidx.annotation.LayoutRes
import androidx.annotation.UiThread
-import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.AppCompatTextView
-import androidx.core.util.Consumer
+import androidx.core.view.ViewCompat
+import androidx.emoji2.emojipicker.Extensions.toItemType
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.tracing.Trace
/** RecyclerView adapter for emoji body. */
internal class EmojiPickerBodyAdapter(
- context: Context,
+ private val context: Context,
private val emojiGridColumns: Int,
private val emojiGridRows: Float,
- private val categoryNames: Array<String>,
- private val onEmojiPickedListener: Consumer<EmojiViewItem>?
+ private val stickyVariantProvider: StickyVariantProvider,
+ internal var flattenSource: ItemViewDataFlatList,
+ private val onEmojiPickedListener: EmojiPickerBodyAdapter.(EmojiViewItem) -> Unit,
) : Adapter<ViewHolder>() {
private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
- private val context = context
-
- private var flattenSource: ItemViewDataFlatList
-
- init {
- val categorizedEmojis: MutableList<MutableList<ItemViewData>> = mutableListOf()
- for (i in categoryNames.indices) {
- categorizedEmojis.add(mutableListOf())
- }
- flattenSource = ItemViewDataFlatList(
- categorizedEmojis,
- emojiGridColumns
- )
- }
@UiThread
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
return try {
- val view: View
- when (viewType) {
- CategorySeparatorViewData.TYPE -> {
- view = layoutInflater.inflate(
- R.layout.category_text_view,
- parent,
- /* attachToRoot= */ false
- )
- view.layoutParams =
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+ when (viewType.toItemType()) {
+ ItemType.CATEGORY_TITLE -> createSimpleHolder(R.layout.category_text_view, parent)
+ ItemType.PLACEHOLDER_TEXT -> createSimpleHolder(
+ R.layout.emoji_picker_empty_category_text_view,
+ parent
+ ) {
+ minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
}
- EmptyCategoryViewData.TYPE -> {
- view = layoutInflater.inflate(
- R.layout.emoji_picker_empty_category_text_view,
- parent,
- /* attachToRoot= */ false
- )
- view.layoutParams =
- LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
- view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
- }
-
- EmojiViewData.TYPE -> {
- return EmojiViewHolder(
+ ItemType.EMOJI -> {
+ EmojiViewHolder(
+ context,
parent,
layoutInflater,
getParentWidth(parent) / emojiGridColumns,
(parent.measuredHeight / emojiGridRows).toInt(),
- onEmojiPickedListener
+ stickyVariantProvider,
+ onEmojiPickedListener = { emojiViewItem ->
+ onEmojiPickedListener(emojiViewItem)
+ },
+ onEmojiPickedFromPopupListener = { emoji ->
+ with(flattenSource[bindingAdapterPosition] as EmojiViewData) {
+ if (updateToVariants) {
+ primary = emoji
+ notifyItemChanged(bindingAdapterPosition)
+ }
+ }
+ }
)
}
- DummyViewData.TYPE -> {
- view = View(context)
- view.layoutParams = LayoutParams(
- getParentWidth(parent) / emojiGridColumns,
- (parent.measuredHeight / emojiGridRows).toInt()
- )
- }
-
- else -> {
- Log.e(
- "EmojiPickerBodyAdapter",
- "EmojiPickerBodyAdapter gets unsupported view type."
- )
- view = View(context)
- view.layoutParams =
- LayoutParams(
- getParentWidth(parent) / emojiGridColumns,
- (parent.measuredHeight / emojiGridRows).toInt()
- )
- }
+ ItemType.PLACEHOLDER_EMOJI -> object : ViewHolder(View(context)) {}
}
- object : ViewHolder(view) {}
} finally {
Trace.endSection()
}
}
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- val viewType = viewHolder.itemViewType
- val view = viewHolder.itemView
- if (viewType == CategorySeparatorViewData.TYPE) {
- val categoryIndex = flattenSource.getCategoryIndex(position)
- val item = flattenSource[position] as CategorySeparatorViewData
- var categoryName = item.categoryName
- if (categoryName.isEmpty()) {
- categoryName = categoryNames[categoryIndex]
+ when (getItemViewType(position).toItemType()) {
+ ItemType.CATEGORY_TITLE ->
+ ViewCompat.requireViewById<AppCompatTextView>(
+ viewHolder.itemView,
+ R.id.category_name
+ ).text =
+ (flattenSource[position] as CategoryTitle).title
+
+ ItemType.PLACEHOLDER_TEXT ->
+ ViewCompat.requireViewById<AppCompatTextView>(
+ viewHolder.itemView,
+ R.id.emoji_picker_empty_category_view
+ ).text =
+ (flattenSource[position] as PlaceholderText).text
+
+ ItemType.EMOJI -> {
+ (viewHolder as EmojiViewHolder).bindEmoji(
+ (flattenSource[position] as EmojiViewData).let {
+ EmojiViewItem(
+ it.primary,
+ it.variants
+ )
+ })
}
- // Show category label.
- val categoryLabel = view.findViewById<AppCompatTextView>(R.id.category_name)
- if (categoryName.isEmpty()) {
- categoryLabel.visibility = View.GONE
- } else {
- categoryLabel.text = categoryName
- categoryLabel.visibility = View.VISIBLE
- }
- } else if (viewType == EmptyCategoryViewData.TYPE) {
- // Show empty category description.
- val emptyCategoryView =
- view.findViewById<AppCompatTextView>(R.id.emoji_picker_empty_category_view)
- val item = flattenSource[position] as EmptyCategoryViewData
- var content = item.description
- if (content.isEmpty()) {
- val categoryIndex: Int = getCategoryIndex(position)
- content = context.getString(
- if (categoryIndex == EmojiPickerConstants.RECENT_CATEGORY_INDEX)
- R.string.emoji_empty_recent_category
- else R.string.emoji_empty_non_recent_category
- )
- }
- emptyCategoryView.text = content
- } else if (viewType == EmojiViewData.TYPE) {
- val item = flattenSource[position] as EmojiViewData
- val emojiViewHolder = viewHolder as EmojiViewHolder
- emojiViewHolder.bindEmoji(
- EmojiViewItem(
- item.primary,
- item.secondaries.toList()
- )
- )
+
+ ItemType.PLACEHOLDER_EMOJI -> {}
}
}
@@ -169,28 +118,29 @@
}
override fun getItemViewType(position: Int): Int {
- return flattenSource[position].type
- }
-
- @IntRange(from = 0)
- fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
- return getFlattenSource().getCategoryIndex(position)
- }
-
- @VisibleForTesting
- fun getFlattenSource(): ItemViewDataFlatList {
- return flattenSource
+ return flattenSource[position].viewType
}
private fun getParentWidth(parent: ViewGroup): Int {
return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
}
- fun updateEmojis(emojis: List<List<ItemViewData>>) {
- flattenSource = ItemViewDataFlatList(
- emojis,
- emojiGridColumns
- )
- notifyDataSetChanged()
- }
+ private fun createSimpleHolder(
+ @LayoutRes layoutId: Int,
+ parent: ViewGroup,
+ init: (View.() -> Unit)? = null,
+ ) = object : ViewHolder(
+ layoutInflater.inflate(
+ layoutId,
+ parent,
+ /* attachToRoot = */ false
+ ).also {
+ it.layoutParams =
+ LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ )
+ init?.invoke(it)
+ }
+ ) {}
}
\ 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/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index e579631..7f9f44f 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -25,9 +25,6 @@
// The default number of body rows.
const val DEFAULT_BODY_ROWS = 7.5f
- // The default minimal number of each body row.
- const val MIN_ROWS_PER_CATEGORY = 1
-
- // The default recent category index number.
- const val RECENT_CATEGORY_INDEX = 0
+ // The default number of rows of recent items held.
+ const val DEFAULT_MAX_RECENT_ITEM_ROWS = 3
}
\ 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 9f57b46..c03eeb8 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
@@ -18,10 +18,12 @@
import android.content.Context
import android.content.res.TypedArray
-import android.os.Trace
import android.util.AttributeSet
import android.widget.FrameLayout
import androidx.core.util.Consumer
+import androidx.emoji2.emojipicker.EmojiPickerConstants.DEFAULT_MAX_RECENT_ITEM_ROWS
+import androidx.emoji2.emojipicker.Extensions.toEmojiViewData
+import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
@@ -61,6 +63,9 @@
field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_COLUMNS
}
+ private val stickyVariantProvider = StickyVariantProvider(context)
+ private var recentEmojiProvider = DefaultRecentEmojiProvider(context)
+
private lateinit var headerView: RecyclerView
private lateinit var bodyView: RecyclerView
private var onEmojiPickedListener: Consumer<EmojiViewItem>? = null
@@ -86,56 +91,52 @@
}
}
- private fun createEmojiPickerBodyAdapter(
- context: Context,
- emojiGridColumns: Int,
- emojiGridRows: Float,
+ private suspend fun createEmojiPickerBodyAdapter(
categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>,
- onEmojiPickedListener: Consumer<EmojiViewItem>?
+ variantToBaseEmojiMap: Map<String, String>,
+ baseToVariantsEmojiMap: Map<String, List<String>>,
+ onEmojiPickedListener: Consumer<EmojiViewItem>?,
+ recentEmojiProvider: RecentEmojiProvider
): EmojiPickerBodyAdapter {
- val categoryNames = mutableListOf<String>()
- val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
- for (i in categorizedEmojiData.indices) {
- categoryNames.add(categorizedEmojiData[i].categoryName)
- categorizedEmojis.add(
- categorizedEmojiData[i].emojiDataList.toMutableList()
+ val recentItems = recentEmojiProvider.getRecentItemList().map {
+ EmojiViewData(
+ it,
+ baseToVariantsEmojiMap[variantToBaseEmojiMap[it]] ?: listOf(),
+ updateToVariants = false,
)
- }
+ }.toMutableList()
+ val recentGroup = ItemGroup(
+ CategoryTitle(context.getString(R.string.emoji_category_recent)),
+ recentItems,
+ forceContentSize = DEFAULT_MAX_RECENT_ITEM_ROWS * emojiGridColumns,
+ emptyPlaceholderItem = PlaceholderText(
+ context.getString(R.string.emoji_empty_recent_category)
+ ),
+ )
+ val itemViewDataFlatList =
+ ItemViewDataFlatList(listOf(recentGroup) + categorizedEmojiData.map { (name, emojis) ->
+ ItemGroup(
+ CategoryTitle(name),
+ emojis.map {
+ EmojiViewData(stickyVariantProvider[it.emoji], it.variants)
+ },
+ )
+ })
val adapter = EmojiPickerBodyAdapter(
context,
emojiGridColumns,
emojiGridRows,
- categoryNames.toTypedArray(),
- onEmojiPickedListener
- )
- adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
-
- return adapter
- }
-
- private fun createEmojiViewData(categorizedEmojis: MutableList<MutableList<EmojiViewItem>>):
- List<List<ItemViewData>> {
- Trace.beginSection("createEmojiViewData")
- return try {
- val listBuilder = mutableListOf<List<ItemViewData>>()
- for ((categoryIndex, sameType) in categorizedEmojis.withIndex()) {
- val builder = mutableListOf<ItemViewData>()
- for ((idInCategory, eachEmoji) in sameType.withIndex()) {
- builder.add(
- EmojiViewData(
- categoryIndex,
- idInCategory,
- eachEmoji.emoji,
- eachEmoji.variants.toTypedArray()
- )
- )
- }
- listBuilder.add(builder.toList())
+ stickyVariantProvider,
+ itemViewDataFlatList,
+ onEmojiPickedListener = { emojiViewItem ->
+ onEmojiPickedListener?.accept(emojiViewItem)
+ recentItems.indexOfFirst { it.primary == emojiViewItem.emoji }
+ .takeIf { it >= 0 }?.let { recentItems.removeAt(it) }
+ recentItems.add(0, emojiViewItem.toEmojiViewData(updateToVariants = false))
+ recentEmojiProvider.insert(emojiViewItem.emoji)
}
- listBuilder.toList()
- } finally {
- Trace.endSection()
- }
+ )
+ return adapter
}
private suspend fun showEmojiPickerView(context: Context) {
@@ -148,20 +149,37 @@
LinearLayoutManager(
context,
LinearLayoutManager.HORIZONTAL,
- /* reverseLayout= */ false
+ /* reverseLayout = */ false
)
headerView.adapter = EmojiPickerHeaderAdapter(context)
// 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 as? EmojiPickerBodyAdapter) ?: return 1
+ return if (adapter.flattenSource[position].occupyEntireRow)
+ emojiGridColumns
+ else 1
+ }
+ }
+ }
val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
+ val variantToBaseEmojiMap = BundledEmojiListLoader.getPrimaryEmojiLookup()
+ val baseToVariantsEmojiMap = BundledEmojiListLoader.getEmojiVariantsLookup()
bodyView.adapter =
createEmojiPickerBodyAdapter(
- context,
- emojiGridColumns,
- emojiGridRows,
categorizedEmojiData,
- onEmojiPickedListener
+ variantToBaseEmojiMap,
+ baseToVariantsEmojiMap,
+ onEmojiPickedListener,
+ recentEmojiProvider
)
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
index bba8664..1e12a47 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -80,6 +80,7 @@
if (value != null) {
post {
drawEmoji(value)
+ contentDescription = value
invalidate()
}
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
deleted file mode 100644
index aace300..0000000
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
+++ /dev/null
@@ -1,60 +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
-
-/** Concrete entry which contains emoji view data. */
-internal class EmojiViewData(
- categoryIndex: Int,
- idInCategory: Int,
- primary: String,
- secondaries: Array<String>
-) :
- ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
- /** The index of category where the emoji view located in. */
- private val categoryIndex: Int
-
- /** The id of this emoji view in the category, usually is the position of the emoji. */
- private val idInCategory: Int
-
- /** Primary key which is used for labeling and for PRESS action. */
- val primary: String
-
- /** Secondary keys which are used for LONG_PRESS action. */
- val secondaries: Array<String>
-
- /**
- * Instantiates a EmojiViewData.
- *
- * @param categoryIndex Used to compute the id.
- * @param idInCategory Used to compute the id.
- * @param primary The default base variant of the given emoji (no skin tone or gender modifier)
- * @param secondaries Array of variants associated to primary
- */
- init {
- this.categoryIndex = categoryIndex
- this.idInCategory = idInCategory
- this.primary = primary
- this.secondaries = secondaries
- }
-
- override val type: Int
- get() = TYPE
-
- companion object {
- val TYPE: Int = EmojiViewData::class.java.name.hashCode()
- }
-}
\ No newline at end of file
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 723c646..f65bf97 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
@@ -16,41 +16,119 @@
package androidx.emoji2.emojipicker
+import android.content.Context
+import android.view.Gravity
import android.view.LayoutInflater
+import android.view.View.GONE
+import android.view.View.OnClickListener
+import android.view.View.OnLongClickListener
+import android.view.View.VISIBLE
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
-import androidx.core.util.Consumer
+import android.view.accessibility.AccessibilityEvent
+import android.widget.GridLayout
+import android.widget.ImageView
+import android.widget.PopupWindow
import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import kotlin.math.roundToInt
/** A [ViewHolder] containing an emoji view and emoji data. */
internal class EmojiViewHolder(
+ context: Context,
parent: ViewGroup,
layoutInflater: LayoutInflater,
width: Int,
height: Int,
- onEmojiPickedListener: Consumer<EmojiViewItem>?
+ private val stickyVariantProvider: StickyVariantProvider,
+ private val onEmojiPickedListener: EmojiViewHolder.(EmojiViewItem) -> Unit,
+ private val onEmojiPickedFromPopupListener: EmojiViewHolder.(String) -> Unit
) : ViewHolder(
layoutInflater
- .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+ .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot = */false)
) {
+ 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 {
+ showPopupWindow(layoutInflater, emojiView) {
+ PopupViewHelper(context).fillPopupView(
+ it,
+ layoutInflater,
+ emojiView.measuredWidth,
+ emojiView.measuredHeight,
+ emojiViewItem.variants,
+ clickListener = { view ->
+ val emojiPickedInPopup = (view as EmojiView).emoji.toString()
+ onEmojiPickedFromPopupListener(emojiPickedInPopup)
+ onEmojiClickListener.onClick(view)
+ // variants[0] is always the base (i.e., primary) emoji
+ stickyVariantProvider.update(emojiViewItem.variants[0], emojiPickedInPopup)
+ this.dismiss()
+ // Hover on the base emoji after popup dismissed
+ emojiView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ )
+ }
+ true
+ }
+
private val emojiView: EmojiView
+ private val indicator: ImageView
private lateinit var emojiViewItem: EmojiViewItem
init {
itemView.layoutParams = LayoutParams(width, height)
emojiView = itemView.findViewById(R.id.emoji_view)
emojiView.isClickable = true
-
- // set emojiViewListener
- emojiView.setOnClickListener {
- onEmojiPickedListener?.accept(emojiViewItem)
- }
+ emojiView.setOnClickListener(onEmojiClickListener)
+ indicator = itemView.findViewById(R.id.variant_availability_indicator)
}
fun bindEmoji(
- emojiViewItem: EmojiViewItem
+ emojiViewItem: EmojiViewItem,
) {
emojiView.emoji = emojiViewItem.emoji
this.emojiViewItem = emojiViewItem
+
+ if (emojiViewItem.variants.isNotEmpty()) {
+ indicator.visibility = VISIBLE
+ emojiView.setOnLongClickListener(onEmojiLongClickListener)
+ emojiView.isLongClickable = true
+ } else {
+ indicator.visibility = GONE
+ emojiView.setOnLongClickListener(null)
+ emojiView.isLongClickable = false
+ }
+ }
+
+ private fun showPopupWindow(
+ layoutInflater: LayoutInflater,
+ parent: EmojiView,
+ init: PopupWindow.(GridLayout) -> Unit
+ ) {
+ val popupView = layoutInflater
+ .inflate(R.layout.variant_popup, null, false)
+ .findViewById<GridLayout>(R.id.variant_popup)
+ PopupWindow(popupView, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, false).apply {
+ init(popupView)
+ val location = IntArray(2)
+ parent.getLocationInWindow(location)
+ // Make the popup view center align with the target emoji view.
+ val x =
+ location[0] + parent.width / 2f - popupView.columnCount * parent.width / 2f
+ val y = location[1] - popupView.rowCount * parent.height
+ isOutsideTouchable = true
+ isTouchable = true
+ animationStyle = R.style.VariantPopupAnimation
+ showAtLocation(
+ parent,
+ Gravity.NO_GRAVITY,
+ x.roundToInt(),
+ y
+ )
+ }
}
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
deleted file mode 100644
index 9b96673..0000000
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
+++ /dev/null
@@ -1,57 +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
-
-/**
- * Indicator to show "You haven't used any emojis yet"-like label for empty category.
- *
- * EmptyCategoryViewData: A full-width `Space` at the beginning of each empty category, to
- * show description and indicate that there is no items in the category.
- */
-internal class EmptyCategoryViewData
-/**
- * Instantiates an EmptyCategoryViewData.
- *
- * @param categoryIndex Used to compute the id.
- * @param idInCategory Used to compute the id.
- * @param description The description showing in the text view, e.g. "You haven't used any emojis
- * yet". If empty, will look up the corresponding description based on `categoryIndex`
- * in [EmojiPickerBodyAdapter.onBindViewHolder].
- */(
- categoryIndex: Int,
- idInCategory: Int,
- /** The description to indicate the category is empty. */
- val description: String
-) :
- ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
- override val type: Int
- get() = TYPE
-
- companion object {
- val TYPE = EmptyCategoryViewData::class.java.name.hashCode()
-
- /**
- * Use -1 as categoryIndex and idInCategory and empty string as description for the default
- * instance. The categoryIndex and idInCategory are just used to compute the id for the instance.
- * Make the description empty to look up the corresponding description based on category index in
- * [EmojiPickerBodyAdapter.onBindViewHolder].
- */
- val INSTANCE = EmptyCategoryViewData( /* categoryIndex= */
- -1, /* idInCategory= */-1, /* description= */""
- )
- }
-}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
index 6df7bae..76a838b 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
@@ -16,23 +16,46 @@
package androidx.emoji2.emojipicker
-import androidx.annotation.IntRange
+internal enum class ItemType {
+ CATEGORY_TITLE,
+ PLACEHOLDER_TEXT,
+ EMOJI,
+ PLACEHOLDER_EMOJI,
+}
-/** Value (immutable) classes for Emoji Picker.*/
-internal abstract class ItemViewData(val id: Long) {
- abstract val type: Int
+/**
+ * Represents an item within the body RecyclerView.
+ */
+internal sealed class ItemViewData(itemType: ItemType, val occupyEntireRow: Boolean = false) {
+ val viewType = itemType.ordinal
+}
- override fun hashCode(): Int {
- return (id xor (id ushr 32)).toInt()
- }
+/**
+ * Title of each category.
+ */
+internal class CategoryTitle(val title: String) :
+ ItemViewData(ItemType.CATEGORY_TITLE, occupyEntireRow = true)
- companion object {
- fun calculateId(
- type: Int,
- @IntRange(from = 0, to = 256) categoryIndex: Int,
- @IntRange(from = 0) idInCategory: Int
- ): Long {
- return type.toLong() shl 60 or (categoryIndex.toLong() shl 32) or idInCategory.toLong()
- }
- }
-}
\ No newline at end of file
+/**
+ * Text to display when the category contains no items.
+ */
+internal class PlaceholderText(val text: String) :
+ ItemViewData(ItemType.PLACEHOLDER_TEXT, occupyEntireRow = true)
+
+/**
+ * Represents an emoji.
+ */
+internal class EmojiViewData(
+ var primary: String,
+ val variants: List<String>,
+ val updateToVariants: Boolean = true
+) : ItemViewData(ItemType.EMOJI)
+
+internal object PlaceholderEmoji : ItemViewData(ItemType.PLACEHOLDER_EMOJI)
+
+internal object Extensions {
+ internal fun Int.toItemType() = ItemType.values()[this]
+
+ internal fun EmojiViewItem.toEmojiViewData(updateToVariants: Boolean = true) =
+ EmojiViewData(emoji, variants, updateToVariants)
+}
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..d150941 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
@@ -20,111 +20,56 @@
import androidx.annotation.IntRange
/**
- * Flattened list of categorized `ItemViewData` (`List<List<ItemViewData>>`) with placeholder
- * entries and category separators.
- *
- * Keyword "position" is defined in `RecyclerView`.
+ * A group of items in RecyclerView.
+ * [titleItem] comes first.
+ * [contentItems] comes after [titleItem].
+ * [emptyPlaceholderItem] will be served after [titleItem] only if [contentItems] is empty.
+ * [forceContentSize], if provided, will truncate [contentItems] to certain size or pad with
+ * [PlaceholderEmoji]s.
+ */
+internal class ItemGroup(
+ private val titleItem: CategoryTitle,
+ private val contentItems: List<EmojiViewData>,
+ private val forceContentSize: Int? = null,
+ private val emptyPlaceholderItem: PlaceholderText? = null
+) {
+
+ val size: Int = 1 /* title */ +
+ (forceContentSize ?: maxOf(contentItems.size, if (emptyPlaceholderItem != null) 1 else 0))
+
+ operator fun get(index: Int): ItemViewData {
+ if (index == 0) return titleItem
+ val contentIndex = index - 1
+ if (contentIndex < contentItems.size) return contentItems[contentIndex]
+ if (contentIndex == 0 && emptyPlaceholderItem != null) return emptyPlaceholderItem
+ return PlaceholderEmoji
+ }
+}
+
+/**
+ * A view of concatenated list of [ItemGroup].
*/
internal class ItemViewDataFlatList(
- categorizedSources: List<List<ItemViewData>>,
- @IntRange(from = 1) columns: Int
-) : AbstractList<ItemViewData>() {
-
+ private val groups: List<ItemGroup>,
+) {
companion object {
const val LOG_TAG = "ItemViewDataFlatList"
}
- override val size: Int
- get() = totalSize
-
- /** Returns number of categories */
- /** # of categories. */
- @get:IntRange(from = 0)
- val numberOfCategories: Int
- private val categorizedSources: MutableList<List<ItemViewData>>
- private val categorySizes: IntArray
- private val categoryStartPositions: IntArray
- private val columns: Int
-
- /** == `size()`, including all types of `ItemViewData`s. */
- private var totalSize = 0
+ val size: Int = groups.sumOf { it.size }
init {
- this.categorizedSources = ArrayList(categorizedSources)
- this.columns = columns
- numberOfCategories = this.categorizedSources.size
- categorySizes = IntArray(numberOfCategories)
- categoryStartPositions = IntArray(numberOfCategories)
- updateIndex()
- if (categorizedSources.isEmpty()) {
+ if (groups.isEmpty()) {
Log.wtf(LOG_TAG, "Initialized with empty categorized sources")
}
}
- private fun updateIndex() {
- var categoryStartPosition = 0
- for (currentCategoryIndex in 0 until numberOfCategories) {
- val sources: List<ItemViewData> = categorizedSources[currentCategoryIndex]
- val sourcesSize: Int = sources.size
- categoryStartPositions[currentCategoryIndex] = categoryStartPosition
- var sourcesSizeIncludingEmpty: Int
- var rowsInCategory = Math.ceil(sourcesSize / columns.toDouble()).toInt()
- // Guarantee showing at least `minRowsPerCategory` rows for each category.
- rowsInCategory = Math.max(rowsInCategory, EmojiPickerConstants.MIN_ROWS_PER_CATEGORY)
- sourcesSizeIncludingEmpty =
- if (sourcesSize <= 0 || sourcesSize == 1 && sources[0] is EmptyCategoryViewData) {
- // category separator(occupy entire row) + empty category indicator(occupy entire row)
- // + placeholder view items
- 1 + 1 + if (rowsInCategory >= 1) (rowsInCategory - 1) * columns else 0
- } else {
- rowsInCategory * columns + 1 // +1 for category separator
- }
- categorySizes[currentCategoryIndex] = sourcesSizeIncludingEmpty
- categoryStartPosition += sourcesSizeIncludingEmpty
+ operator fun get(@IntRange(from = 0) absolutePosition: Int): ItemViewData {
+ var localPosition = absolutePosition
+ for (group in groups) {
+ if (localPosition < group.size) return group[localPosition]
+ else localPosition -= group.size
}
- totalSize = categoryStartPosition
- }
-
- override fun get(@IntRange(from = 0) index: Int): ItemViewData {
- val currentCategoryIndex = getCategoryIndex(index)
- val indexInCategory = index - categoryStartPositions[currentCategoryIndex]
- return if (indexInCategory < 0) {
- Log.wtf(
- LOG_TAG,
- String.format(
- "position (%d) for category (%d) is invalid",
- index,
- currentCategoryIndex
- )
-
- )
- DummyViewData.INSTANCE
- } else if (indexInCategory == 0) {
- // Category separator occupies first place.
- CategorySeparatorViewData(
- currentCategoryIndex, indexInCategory, /* categoryName= */""
- )
- } else if (indexInCategory < categorizedSources[currentCategoryIndex].size + 1) {
- // Concrete ItemViewData.
- categorizedSources[currentCategoryIndex][indexInCategory - 1]
- } else if (indexInCategory == 1 && categorizedSources[currentCategoryIndex].isEmpty()) {
- // Empty category indicator.
- EmptyCategoryViewData.INSTANCE
- } else {
- // Placeholder entries located at the end of category.
- DummyViewData.INSTANCE
- }
- }
-
- /** Returns category index for given `position` */
- @IntRange(from = 0)
- fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
- var currentCategoryIndex = 0
- while (currentCategoryIndex + 1 < numberOfCategories &&
- position >= categoryStartPositions[currentCategoryIndex + 1]
- ) {
- currentCategoryIndex++
- }
- return currentCategoryIndex
+ throw IndexOutOfBoundsException()
}
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
new file mode 100644
index 0000000..af0f9cb
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/PopupViewHelper.kt
@@ -0,0 +1,176 @@
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.OnClickListener
+import android.view.accessibility.AccessibilityEvent
+import android.widget.FrameLayout
+import android.widget.GridLayout
+import androidx.core.content.ContextCompat
+
+/*
+ * 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.
+ */
+
+internal class PopupViewHelper(private val context: Context) {
+ companion object {
+ private enum class Layout { FLAT, SQUARE, SQUARE_WITH_SKIN_TONE_CIRCLE }
+
+ private const val SQUARE_LAYOUT_VARIANT_COUNT = 26
+
+ // Set of emojis that use the square layout without skin tone.
+ private val SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE = setOf("👪")
+
+ private val SKIN_TONE_COLOR_RES_IDS = listOf(
+ R.color.light_skin_tone,
+ R.color.medium_light_skin_tone,
+ R.color.medium_skin_tone,
+ R.color.medium_dark_skin_tone,
+ R.color.dark_skin_tone
+ )
+
+ /**
+ * Square variant layout template with skin tone.
+ * 0 : a place holder
+ * -5: light skin tone circle
+ * -4: medium-light skin tone circle
+ * -3: medium skin tone circle
+ * -2: medium-dark skin tone circle
+ * -1: dark skin tone circle
+ * Positive number is the index + 1 in the variant array
+ */
+ private val SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE = arrayOf(
+ intArrayOf(0, 0, -5, -4, -3, -2, -1),
+ intArrayOf(0, -5, 2, 3, 4, 5, 6),
+ intArrayOf(0, -4, 7, 8, 9, 10, 11),
+ intArrayOf(0, -3, 12, 13, 14, 15, 16),
+ intArrayOf(0, -2, 17, 18, 19, 20, 21),
+ intArrayOf(1, -1, 22, 23, 24, 25, 26)
+ )
+
+ /**
+ * Square variant layout template without skin tone.
+ * 0 : a place holder
+ * Positive number is the index + 1 in the variant array
+ */
+ private val SQUARE_LAYOUT_TEMPLATE = arrayOf(
+ intArrayOf(0, 2, 3, 4, 5, 6),
+ intArrayOf(0, 7, 8, 9, 10, 11),
+ intArrayOf(0, 12, 13, 14, 15, 16),
+ intArrayOf(0, 17, 18, 19, 20, 21),
+ intArrayOf(1, 22, 23, 24, 25, 26)
+ )
+ }
+
+ fun fillPopupView(
+ popupView: GridLayout,
+ layoutInflater: LayoutInflater,
+ gridWidth: Int,
+ gridHeight: Int,
+ variants: List<String>,
+ clickListener: OnClickListener
+ ): GridLayout {
+ val gridTemplate = getGridTemplate(variants)
+ popupView
+ .apply {
+ columnCount = gridTemplate.column
+ rowCount = gridTemplate.row
+ orientation = GridLayout.HORIZONTAL
+ }
+ gridTemplate.template.flatMap { it.asIterable() }.forEach {
+ val gridCell = when (it) {
+ in 1..variants.size -> (layoutInflater.inflate(
+ R.layout.emoji_view_holder,
+ null,
+ false
+ ) as FrameLayout).apply {
+ (this.getChildAt(0) as EmojiView).apply {
+ emoji = variants[it - 1]
+ setOnClickListener(clickListener)
+ if (it == 1) {
+ // Hover on the first emoji in the popup
+ popupView.post {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
+ }
+ }
+ }
+ }
+
+ 0 -> layoutInflater
+ .inflate(R.layout.emoji_view_holder, null, false)
+
+ else -> SkinToneCircleViewView(context).apply {
+ paint = Paint().apply {
+ color = ContextCompat.getColor(context, SKIN_TONE_COLOR_RES_IDS[it + 5])
+ style = Paint.Style.FILL
+ }
+ }
+ }
+ popupView.addView(gridCell)
+ gridCell.layoutParams.width = gridWidth
+ gridCell.layoutParams.height = gridHeight
+ }
+ return popupView
+ }
+
+ private fun getGridTemplate(variants: List<String>): GridTemplate {
+ val layout =
+ if (variants.size == SQUARE_LAYOUT_VARIANT_COUNT)
+ if (SQUARE_LAYOUT_EMOJI_NO_SKIN_TONE.contains(variants[0]))
+ Layout.SQUARE
+ else Layout.SQUARE_WITH_SKIN_TONE_CIRCLE
+ else Layout.FLAT
+ val template = when (layout) {
+ Layout.SQUARE -> SQUARE_LAYOUT_TEMPLATE
+ Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> SQUARE_LAYOUT_WITH_SKIN_TONES_TEMPLATE
+ Layout.FLAT -> arrayOf(variants.indices.map { it + 1 }.toIntArray())
+ }
+ val column = when (layout) {
+ Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template[0].size
+ Layout.FLAT -> minOf(6, template[0].size)
+ }
+ val row = when (layout) {
+ Layout.SQUARE, Layout.SQUARE_WITH_SKIN_TONE_CIRCLE -> template.size
+ Layout.FLAT -> variants.size / column + if (variants.size % column == 0) 0 else 1
+ }
+
+ return GridTemplate(template, row, column)
+ }
+
+ private data class GridTemplate(
+ val template: Array<IntArray>,
+ val row: Int,
+ val column: Int
+ )
+}
+
+internal class SkinToneCircleViewView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null
+) : View(context, attrs) {
+ private val radius = resources.getDimension(R.dimen.emoji_picker_skin_tone_circle_radius)
+ var paint: Paint? = null
+
+ override fun draw(canvas: Canvas?) {
+ super.draw(canvas)
+ canvas?.apply {
+ paint?.let { drawCircle(width / 2f, height / 2f, radius, it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
similarity index 60%
rename from emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt
rename to emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
index 1218fef..56a756c 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/RecentEmojiProvider.kt
@@ -16,17 +16,13 @@
package androidx.emoji2.emojipicker
-/**
- * Placeholder entry, filled at the end of each category if there are room to be filled.
- *
- * This behaves like NaN. Nothing is equal to Placeholder entry.
- */
-internal class DummyViewData(id: Long) : ItemViewData(id) {
- override val type: Int
- get() = TYPE
+/** 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)
- companion object {
- val TYPE: Int = DummyViewData::class.java.name.hashCode()
- val INSTANCE = DummyViewData(TYPE.toLong())
- }
+ /** 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/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.kt
new file mode 100644
index 0000000..63c7530
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/StickyVariantProvider.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
+
+/**
+ * A class that handles user's emoji variant selection using SharedPreferences.
+ */
+internal class StickyVariantProvider(context: Context) {
+ companion object {
+ const val PREFERENCES_FILE_NAME = "androidx.emoji2.emojipicker.preferences"
+ const val STICKY_VARIANT_PROVIDER_KEY = "pref_key_sticky_variant"
+ const val KEY_VALUE_DELIMITER = "="
+ const val ENTRY_DELIMITER = "|"
+ }
+
+ private val sharedPreferences =
+ context.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE)
+
+ private val stickyVariantMap: Map<String, String> by lazy {
+ sharedPreferences.getString(STICKY_VARIANT_PROVIDER_KEY, null)?.split(ENTRY_DELIMITER)
+ ?.associate { entry ->
+ entry.split(KEY_VALUE_DELIMITER, limit = 2).takeIf { it.size == 2 }
+ ?.let { it[0] to it[1] } ?: ("" to "")
+ } ?: mapOf()
+ }
+
+ internal operator fun get(emoji: String): String = stickyVariantMap[emoji] ?: emoji
+
+ internal fun update(baseEmoji: String, variantClicked: String) {
+ stickyVariantMap.toMutableMap().apply {
+ if (baseEmoji == variantClicked) {
+ this.remove(baseEmoji)
+ } else {
+ this[baseEmoji] = variantClicked
+ }
+ sharedPreferences.edit()
+ .putString(
+ STICKY_VARIANT_PROVIDER_KEY,
+ entries.joinToString(ENTRY_DELIMITER)
+ ).commit()
+ }
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml
new file mode 100644
index 0000000..39f3163
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_down_and_fade_out.xml
@@ -0,0 +1,31 @@
+<?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.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true"
+ android:interpolator="@android:anim/accelerate_interpolator">
+ <alpha
+ android:duration="100"
+ android:fromAlpha="1" android:toAlpha="0" />
+
+ <scale
+ android:duration="100"
+ android:fromXScale="1.0" android:toXScale="1.0"
+ android:fromYScale="1.0" android:toYScale="0.0"
+ android:pivotX="0%"
+ android:pivotY="100%" />
+</set>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml
new file mode 100644
index 0000000..5dad0a8
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/anim/slide_up_and_fade_in.xml
@@ -0,0 +1,31 @@
+<?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.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fillAfter="true"
+ android:interpolator="@android:anim/decelerate_interpolator">
+ <alpha
+ android:duration="100"
+ android:fromAlpha="0.5" android:toAlpha="1" />
+
+ <scale
+ android:duration="100"
+ android:fromXScale="1.0" android:toXScale="1.0"
+ android:fromYScale="0.0" android:toYScale="1.0"
+ android:pivotX="0%"
+ android:pivotY="100%" />
+</set>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
new file mode 100644
index 0000000..c625912
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/popup_view_rounded_background.xml
@@ -0,0 +1,21 @@
+<?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.
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#FFDDDDDD"/>
+ <corners android:radius="@dimen/emoji_picker_popup_view_holder_corner_radius"/>
+</shape>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
new file mode 100644
index 0000000..e27fe2e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/variant_availability_indicator.xml
@@ -0,0 +1,26 @@
+<!--
+ 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M2,22h20V2L2,22z"/>
+</vector>
\ 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/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
index 0fa8c9c..76fe2b9 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -15,7 +15,6 @@
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0dp"
android:layout_height="0dp">
@@ -28,4 +27,11 @@
android:importantForAccessibility="yes"
android:textSize="30dp"
tools:ignore="SpUsage" />
+ <ImageView
+ android:id="@+id/variant_availability_indicator"
+ android:visibility="gone"
+ android:src="@drawable/variant_availability_indicator"
+ android:layout_width="5dp"
+ android:layout_height="5dp"
+ android:layout_gravity="bottom|end"/>
</FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
new file mode 100644
index 0000000..d61780e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/variant_popup.xml
@@ -0,0 +1,23 @@
+<?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.
+ -->
+<GridLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/variant_popup"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ style="@style/EmojiPickerStylePopupViewHolder">
+</GridLayout>
\ No newline at end of file
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/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml b/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml
new file mode 100644
index 0000000..fb08439
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?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.
+ -->
+
+<resources>
+ <color name="light_skin_tone">#edbd82</color>
+ <color name="medium_light_skin_tone">#ba8f63</color>
+ <color name="medium_skin_tone">#91674d</color>
+ <color name="medium_dark_skin_tone">#875334</color>
+ <color name="dark_skin_tone">#4a2f27</color>
+</resources>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index cc79c54..17afb30 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -24,4 +24,12 @@
<dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
<dimen name="emoji_picker_header_height">50dp</dimen>
<dimen name="emoji_picker_header_padding">8dp</dimen>
+ <dimen name="variant_availability_indicator_height">5dp</dimen>
+ <dimen name="variant_availability_indicator_width">5dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_vertical">3dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_start">3dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_padding_end">5dp</dimen>
+ <dimen name="emoji_picker_popup_view_elevation">2dp</dimen>
+ <dimen name="emoji_picker_popup_view_holder_corner_radius">10dp</dimen>
+ <dimen name="emoji_picker_skin_tone_circle_radius">6dp</dimen>
</resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index 472702f..c883441 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -15,6 +15,8 @@
-->
<resources>
+ <!-- Emoji recent sub category. -->
+ <string name="emoji_category_recent">RECENTLY USED</string>
<!-- Emoji keyboard's smileys and emotions sub category. -->
<string name="emoji_category_emotions">SMILEYS AND EMOTIONS</string>
<!-- Emoji keyboard's people sub category. -->
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index 0e6d24e..f7df2be 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -31,4 +31,18 @@
<style name="EmojiPickerStyleCategoryLabelText">
<item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
</style>
+
+ <style name="EmojiPickerStylePopupViewHolder">
+ <item name="android:padding">@dimen/emoji_picker_popup_view_holder_padding_vertical</item>
+ <item name="android:paddingStart">@dimen/emoji_picker_popup_view_holder_padding_start</item>
+ <item name="android:paddingEnd">@dimen/emoji_picker_popup_view_holder_padding_end</item>
+ <item name="android:background">@drawable/popup_view_rounded_background</item>
+ <item name="android:elevation">@dimen/emoji_picker_popup_view_elevation</item>
+ </style>
+
+ <style name="VariantPopupAnimation">
+ <item name="android:windowEnterAnimation">@anim/slide_up_and_fade_in</item>
+ <item name="android:windowExitAnimation">@anim/slide_down_and_fade_out</item>
+ </style>
+
</resources>
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp b/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
index 35a8dfc..e712d0d 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/webp_with_anim_without_exif.webp
Binary files differ
diff --git a/glance/glance-appwidget/integration-tests/template-demos/build.gradle b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
index 6f5933f..ec43ef6 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/build.gradle
+++ b/glance/glance-appwidget/integration-tests/template-demos/build.gradle
@@ -27,6 +27,8 @@
implementation(libs.kotlinStdlib)
implementation(project(":glance:glance"))
implementation(project(":glance:glance-appwidget"))
+ implementation(project(":glance:glance-material"))
+ implementation(project(":glance:glance-material3"))
implementation("androidx.activity:activity:1.4.0")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.compose.material:material:1.1.0-beta02")
@@ -36,6 +38,7 @@
implementation("androidx.compose.material:material:1.1.0-beta02")
implementation("androidx.datastore:datastore-preferences-core:1.0.0")
implementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+ implementation "androidx.compose.material3:material3:1.0.0"
}
android {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 189229d..0036785 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -91,7 +91,7 @@
</receiver>
<receiver
- android:name="androidx.glance.appwidget.template.demos.FullHeaderListReceiver"
+ android:name="androidx.glance.appwidget.template.demos.FullHeaderThemedListReceiver"
android:enabled="@bool/glance_appwidget_available"
android:exported="false"
android:label="@string/list_style_with_header">
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
index 464d336..fd1b18a 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/ListDemoWidget.kt
@@ -64,11 +64,11 @@
/**
* List demo with list items in full details and list header without action button using data and
- * list template from [BaseListDemoWidget].
+ * list template from [BaseListDemoWidget] with custom theme.
*/
-class FullHeaderListDemoWidget : BaseListDemoWidget() {
+class FullHeaderListThemedDemoWidget : BaseListDemoWidget() {
@Composable
- override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true)
+ override fun TemplateContent() = ListTemplateContent(ListStyle.Full, true, true)
}
/**
@@ -93,8 +93,8 @@
override val glanceAppWidget: GlanceAppWidget = FullHeaderActionListDemoWidget()
}
-class FullHeaderListReceiver : GlanceAppWidgetReceiver() {
- override val glanceAppWidget: GlanceAppWidget = FullHeaderListDemoWidget()
+class FullHeaderThemedListReceiver : GlanceAppWidgetReceiver() {
+ override val glanceAppWidget: GlanceAppWidget = FullHeaderListThemedDemoWidget()
}
class NoHeaderListReceiver : GlanceAppWidgetReceiver() {
@@ -124,14 +124,16 @@
* @param listStyle styling the list by [ListStyle] based data details
* @param initialNumItems initial number of list items to generate in the demo
* @param showHeader whether to show list header as a whole
+ * @param customTheme whether to override Glance system theme with custom theme
*/
@Composable
internal fun ListTemplateContent(
listStyle: ListStyle,
showHeader: Boolean = false,
+ customTheme: Boolean = false,
initialNumItems: Int = MAX_ITEMS,
) {
- GlanceTheme {
+ GlanceTheme(if (customTheme) PalmLeafScheme.colors else GlanceTheme.colors) {
val state = currentState<Preferences>()
val content = mutableListOf<ListTemplateItem>()
for (i in 1..(state[CountKey] ?: initialNumItems)) {
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
new file mode 100644
index 0000000..b5a4029
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/PalmLeafScheme.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget.template.demos
+
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.Color
+import androidx.glance.material3.ColorProviders
+
+/**
+ * Custom Color theme for the Palm Leaf themed demo templates
+ */
+object PalmLeafScheme {
+ val md_theme_light_primary = Color(0xFF026E00)
+ val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+ val md_theme_light_primaryContainer = Color(0xFFD7E8CD)
+ val md_theme_light_onPrimaryContainer = Color(0xFF002200)
+ val md_theme_light_secondary = Color(0xFFA900A9)
+ val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+ val md_theme_light_secondaryContainer = Color(0xFFFFD7F5)
+ val md_theme_light_onSecondaryContainer = Color(0xFF380038)
+ val md_theme_light_tertiary = Color(0xFF006A6A)
+ val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+ val md_theme_light_tertiaryContainer = Color(0xFF00FBFB)
+ val md_theme_light_onTertiaryContainer = Color(0xFF002020)
+ val md_theme_light_error = Color(0xFFBA1A1A)
+ val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+ val md_theme_light_onError = Color(0xFFFFFFFF)
+ val md_theme_light_onErrorContainer = Color(0xFF410002)
+ val md_theme_light_background = Color(0xFFFFFBFF)
+ val md_theme_light_onBackground = Color(0xFF1E1C00)
+ val md_theme_light_surface = Color(0xFFFFFBFF)
+ val md_theme_light_onSurface = Color(0xFF1E1C00)
+ val md_theme_light_surfaceVariant = Color(0xFFDFE4D7)
+ val md_theme_light_onSurfaceVariant = Color(0xFF43483F)
+ val md_theme_light_outline = Color(0xFF73796E)
+ val md_theme_light_inverseOnSurface = Color(0xFFFFF565)
+ val md_theme_light_inverseSurface = Color(0xFF353200)
+ val md_theme_light_inversePrimary = Color(0xFF02E600)
+ val md_theme_light_shadow = Color(0xFF000000)
+ val md_theme_light_surfaceTint = Color(0xFF026E00)
+
+ val md_theme_dark_primary = Color(0xFF02E600)
+ val md_theme_dark_onPrimary = Color(0xFF013A00)
+ val md_theme_dark_primaryContainer = Color(0xFFD7E8CD)
+ val md_theme_dark_onPrimaryContainer = Color(0xFF77FF61)
+ val md_theme_dark_secondary = Color(0xFFFFABF3)
+ val md_theme_dark_onSecondary = Color(0xFF5B005B)
+ val md_theme_dark_secondaryContainer = Color(0xFF810081)
+ val md_theme_dark_onSecondaryContainer = Color(0xFFFFD7F5)
+ val md_theme_dark_tertiary = Color(0xFF00DDDD)
+ val md_theme_dark_onTertiary = Color(0xFF003737)
+ val md_theme_dark_tertiaryContainer = Color(0xFF004F4F)
+ val md_theme_dark_onTertiaryContainer = Color(0xFF00FBFB)
+ val md_theme_dark_error = Color(0xFFFFB4AB)
+ val md_theme_dark_errorContainer = Color(0xFF93000A)
+ val md_theme_dark_onError = Color(0xFF690005)
+ val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+ val md_theme_dark_background = Color(0xFF1E1C00)
+ val md_theme_dark_onBackground = Color(0xFFF2E720)
+ val md_theme_dark_surface = Color(0xFF1E1C00)
+ val md_theme_dark_onSurface = Color(0xFFF2E720)
+ val md_theme_dark_surfaceVariant = Color(0xFF43483F)
+ val md_theme_dark_onSurfaceVariant = Color(0xFFC3C8BC)
+ val md_theme_dark_outline = Color(0xFF8D9387)
+ val md_theme_dark_inverseOnSurface = Color(0xFF1E1C00)
+ val md_theme_dark_inverseSurface = Color(0xFFF2E720)
+ val md_theme_dark_inversePrimary = Color(0xFF026E00)
+ val md_theme_dark_shadow = Color(0xFF000000)
+ val md_theme_dark_surfaceTint = Color(0xFF02E600)
+
+ val seed = Color(0xFF00FF00)
+
+ val LightColors = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ onError = md_theme_light_onError,
+ errorContainer = md_theme_light_errorContainer,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseSurface = md_theme_light_inverseSurface,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ )
+
+ val DarkColors = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ onError = md_theme_dark_onError,
+ errorContainer = md_theme_dark_errorContainer,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+ )
+
+ val colors = ColorProviders(
+ light = LightColors,
+ dark = DarkColors
+ )
+}
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
index 4ee5288..0c49cae 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/values/strings.xml
@@ -27,7 +27,7 @@
<!-- Styles of the List Template widgets -->
<string name="list_style_with_header_action">List Template with header and action demo</string>
- <string name="list_style_with_header">List Template with header demo</string>
+ <string name="list_style_with_header">List Template with header and custom theme demo</string>
<string name="list_style_no_header">List Template with no header demo</string>
<string name="list_style_brief">List Template with no header demo in brief info</string>
diff --git a/gradle.properties b/gradle.properties
index c6660ac..c2082a6 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -44,6 +44,8 @@
android.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.dependencyResolutionAtConfigurationTime.disallow,android.experimental.lint.missingBaselineIsEmptyBaseline
# Workaround for b/162074215
android.includeDependencyInfoInApks=false
+# Allow multiple r8 tasks at once because otherwise they can make the critical path longer: b/256187923
+android.r8.maxWorkers=2
kotlin.stdlib.default.dependency=false
# mac targets cannot be built on linux, suppress the warning.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index de0f8fc..d7c6e4d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -35,7 +35,7 @@
incap = "0.2"
jcodec = "0.2.5"
kotlin = "1.7.21"
-kotlinBenchmark = "0.4.5"
+kotlinBenchmark = "0.4.6"
kotlinNative = "1.7.21"
kotlinCompileTesting = "1.4.9"
kotlinCoroutines = "1.6.4"
@@ -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/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 764b8b2e..25322cf 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -809,17 +809,17 @@
<sha256 value="4df94aaeee8d900be431386e31ef44e82a66e57c3ae30866aec2875aff01fe70" origin="Generated by Gradle"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
- <artifact name="kotlinx-benchmark-plugin-0.4.5.jar">
- <sha256 value="64042e32a7c23040980d8b7cb9c2dc2281bf6e19da511d054639f44ba5eaac66" origin="Generated by Gradle because artifact wasn't signed"/>
+ <component group="org.jetbrains.kotlinx" name="kotlinx-benchmark-plugin" version="0.4.6">
+ <artifact name="kotlinx-benchmark-plugin-0.4.6.jar">
+ <sha256 value="9806179c613658456da5a53cb945138ee0983ecccc9185de22ece2be9c14cfc7" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
- <artifact name="kotlinx-benchmark-plugin-0.4.5.pom">
- <sha256 value="33a06eec241b0a20bc3f37b32574d1aaed34905005daba892a85cd832de14700" origin="Generated by Gradle because artifact wasn't signed"/>
+ <artifact name="kotlinx-benchmark-plugin-0.4.6.module">
+ <sha256 value="b2bcac512573b085113a2ad74ed339b7cdefc49094b342a463b5a51b07ef5e1e" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
- <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.5" androidx:reason="https://youtrack.jetbrains.com/issue/KT-53461">
- <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.5.pom">
- <sha256 value="b13fc9975c2f5cfb992f423c60be042252a289e0f40390e7da2122ac2dec08a5" origin="Generated by Gradle because artifact wasn't signed"/>
+ <component group="org.jetbrains.kotlinx.benchmark" name="org.jetbrains.kotlinx.benchmark.gradle.plugin" version="0.4.6">
+ <artifact name="org.jetbrains.kotlinx.benchmark.gradle.plugin-0.4.6.pom">
+ <sha256 value="3f739c3504155f5568c26c5c013aa62dc254daa7fe627d7b43b3448c79d36447" origin="Generated by Gradle because artifact wasn't signed"/>
</artifact>
</component>
<component group="org.ow2" name="ow2" version="1.5" androidx:reason="https://gitlab.ow2.org/asm/asm/-/merge_requests/354">
diff --git a/graphics/graphics-core/api/current.txt b/graphics/graphics-core/api/current.txt
index 9676208..7000b35 100644
--- a/graphics/graphics-core/api/current.txt
+++ b/graphics/graphics-core/api/current.txt
@@ -336,6 +336,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/public_plus_experimental_current.txt b/graphics/graphics-core/api/public_plus_experimental_current.txt
index 9676208..7000b35 100644
--- a/graphics/graphics-core/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-core/api/public_plus_experimental_current.txt
@@ -336,6 +336,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/api/restricted_current.txt b/graphics/graphics-core/api/restricted_current.txt
index d5c1358..0ee95a4 100644
--- a/graphics/graphics-core/api/restricted_current.txt
+++ b/graphics/graphics-core/api/restricted_current.txt
@@ -337,6 +337,7 @@
method public static void glEGLImageTargetTexture2DOES(int target, androidx.opengl.EGLImageKHR image);
method public static java.util.Set<java.lang.String> parseExtensions(String queryString);
field public static final androidx.opengl.EGLExt.Companion Companion;
+ field public static final String EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer";
field public static final String EGL_ANDROID_IMAGE_NATIVE_BUFFER = "EGL_ANDROID_image_native_buffer";
field public static final String EGL_ANDROID_NATIVE_FENCE_SYNC = "EGL_ANDROID_native_fence_sync";
field public static final int EGL_CONDITION_SATISFIED_KHR = 12534; // 0x30f6
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
index b7c29c1..2e54e92 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/egl/EGLManagerTest.kt
@@ -30,6 +30,7 @@
import androidx.annotation.RequiresApi
import androidx.opengl.EGLBindings
import androidx.opengl.EGLExt
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_CLIENT_BUFFER
import androidx.opengl.EGLExt.Companion.EGL_SYNC_CONDITION_KHR
import androidx.opengl.EGLExt.Companion.EGL_SYNC_FENCE_KHR
import androidx.opengl.EGLExt.Companion.EGL_SYNC_NATIVE_FENCE_ANDROID
@@ -399,9 +400,14 @@
isExtensionSupported(EGL_KHR_IMAGE_BASE)
val androidImageNativeBufferSupported =
isExtensionSupported(EGL_ANDROID_IMAGE_NATIVE_BUFFER)
+ val eglClientBufferSupported =
+ isExtensionSupported(EGL_ANDROID_CLIENT_BUFFER)
// According to EGL spec both these extensions are required in order to support
// eglGetNativeClientBufferAndroid
- if (khrImageBaseSupported && androidImageNativeBufferSupported) {
+ if (khrImageBaseSupported &&
+ androidImageNativeBufferSupported &&
+ eglClientBufferSupported
+ ) {
assertTrue(EGLBindings.nSupportsEglGetNativeClientBufferAndroid())
}
}
diff --git a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
index 0f48ee3..a2015a1 100644
--- a/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
+++ b/graphics/graphics-core/src/main/java/androidx/opengl/EGLExt.kt
@@ -184,6 +184,14 @@
const val EGL_KHR_IMAGE_BASE = "EGL_KHR_image_base"
/**
+ * Extension that allows creating an EGLClientBuffer from an Android [HardwareBuffer]
+ * object which can later be used to create an [EGLImageKHR] instance.
+ * See:
+ * https://registry.khronos.org/EGL/extensions/ANDROID/EGL_ANDROID_get_native_client_buffer.txt
+ */
+ const val EGL_ANDROID_CLIENT_BUFFER = "EGL_ANDROID_get_native_client_buffer"
+
+ /**
* Extension that defines a new EGL resource type that is suitable for
* sharing 2D arrays of image data between client APIs, the EGLImage,
* and allows creating EGLImages from EGL native pixmaps.
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 0349123..b7feac7 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -281,6 +281,18 @@
property public java.time.ZoneOffset? zoneOffset;
}
+ public final class BodyWaterMassRecord implements androidx.health.connect.client.records.Record {
+ ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public androidx.health.connect.client.units.Mass getMass();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final androidx.health.connect.client.units.Mass mass;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
property public final java.time.Instant time;
}
+ public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.Record {
+ ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public double getHeartRateVariabilityMillis();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final double heartRateVariabilityMillis;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class HeightRecord implements androidx.health.connect.client.records.Record {
ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
public static final class HeightRecord.Companion {
}
- public final class HipCircumferenceRecord implements androidx.health.connect.client.records.Record {
- ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class HydrationRecord implements androidx.health.connect.client.records.Record {
ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
@@ -1213,18 +1225,6 @@
public static final class Vo2MaxRecord.Companion {
}
- public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
- ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class WeightRecord implements androidx.health.connect.client.records.Record {
ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 0349123..b7feac7 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -281,6 +281,18 @@
property public java.time.ZoneOffset? zoneOffset;
}
+ public final class BodyWaterMassRecord implements androidx.health.connect.client.records.Record {
+ ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public androidx.health.connect.client.units.Mass getMass();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final androidx.health.connect.client.units.Mass mass;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class BoneMassRecord implements androidx.health.connect.client.records.Record {
ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
property public final java.time.Instant time;
}
+ public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.Record {
+ ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public double getHeartRateVariabilityMillis();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final double heartRateVariabilityMillis;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class HeightRecord implements androidx.health.connect.client.records.Record {
ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
public static final class HeightRecord.Companion {
}
- public final class HipCircumferenceRecord implements androidx.health.connect.client.records.Record {
- ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class HydrationRecord implements androidx.health.connect.client.records.Record {
ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
@@ -1213,18 +1225,6 @@
public static final class Vo2MaxRecord.Companion {
}
- public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
- ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class WeightRecord implements androidx.health.connect.client.records.Record {
ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 3be3faa..372b86f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -281,6 +281,18 @@
property public java.time.ZoneOffset? zoneOffset;
}
+ public final class BodyWaterMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
+ ctor public BodyWaterMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public androidx.health.connect.client.units.Mass getMass();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final androidx.health.connect.client.units.Mass mass;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class BoneMassRecord implements androidx.health.connect.client.records.InstantaneousRecord {
ctor public BoneMassRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass mass, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass getMass();
@@ -648,6 +660,18 @@
property public final java.time.Instant time;
}
+ public final class HeartRateVariabilityRmssdRecord implements androidx.health.connect.client.records.InstantaneousRecord {
+ ctor public HeartRateVariabilityRmssdRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double heartRateVariabilityMillis, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public double getHeartRateVariabilityMillis();
+ method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
+ method public java.time.Instant getTime();
+ method public java.time.ZoneOffset? getZoneOffset();
+ property public final double heartRateVariabilityMillis;
+ property public androidx.health.connect.client.records.metadata.Metadata metadata;
+ property public java.time.Instant time;
+ property public java.time.ZoneOffset? zoneOffset;
+ }
+
public final class HeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
ctor public HeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length height, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Length getHeight();
@@ -667,18 +691,6 @@
public static final class HeightRecord.Companion {
}
- public final class HipCircumferenceRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public HipCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class HydrationRecord implements androidx.health.connect.client.records.IntervalRecord {
ctor public HydrationRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, androidx.health.connect.client.units.Volume volume, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
@@ -1236,18 +1248,6 @@
public static final class Vo2MaxRecord.Companion {
}
- public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public WaistCircumferenceRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Length circumference, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public androidx.health.connect.client.units.Length getCircumference();
- method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public java.time.Instant getTime();
- method public java.time.ZoneOffset? getZoneOffset();
- property public final androidx.health.connect.client.units.Length circumference;
- property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public java.time.Instant time;
- property public java.time.ZoneOffset? zoneOffset;
- }
-
public final class WeightRecord implements androidx.health.connect.client.records.InstantaneousRecord {
ctor public WeightRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Mass weight, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
index c46985a..787b426 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyWaterMassRecord.kt
@@ -15,7 +15,6 @@
*/
package androidx.health.connect.client.records
-import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.units.Mass
import androidx.health.connect.client.units.kilograms
@@ -25,7 +24,6 @@
/**
* Captures the user's body water mass. Each record represents a single instantaneous measurement.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class BodyWaterMassRecord(
override val time: Instant,
override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
index a5379ee..4728b73 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HeartRateVariabilityRmssdRecord.kt
@@ -15,7 +15,6 @@
*/
package androidx.health.connect.client.records
-import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -23,10 +22,7 @@
/**
* Captures user's heart rate variability (HRV) as measured by the root mean square of successive
* differences (RMSSD) between normal heartbeats.
- *
- * @suppress
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class HeartRateVariabilityRmssdRecord(
override val time: Instant,
override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
index a261871..bda2aaf 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/HipCircumferenceRecord.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.units.Length
import androidx.health.connect.client.units.meters
@@ -22,6 +23,7 @@
import java.time.ZoneOffset
/** Captures the user's hip circumference. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class HipCircumferenceRecord(
override val time: Instant,
override val zoneOffset: ZoneOffset?,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
index b04a2c4..c8e6244 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/WaistCircumferenceRecord.kt
@@ -15,6 +15,7 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.records.metadata.Metadata
import androidx.health.connect.client.units.Length
import androidx.health.connect.client.units.meters
@@ -26,6 +27,7 @@
*
* See [Length] for supported units.
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class WaistCircumferenceRecord(
override val time: Instant,
override val zoneOffset: ZoneOffset?,
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt
new file mode 100644
index 0000000..2de499a
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseCapabilitiesTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.health.services.client.data
+
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableSet
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseCapabilitiesTest {
+ @Test
+ fun return_supportedDataTypesForSpecifiedExercise() {
+ assertThat(
+ EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+ ExerciseType.WALKING
+ ).supportedDataTypes
+ ).isEqualTo(
+ ImmutableSet.of(DataType.STEPS)
+ )
+ }
+
+ @Test
+ fun supportedGoalsForExercise() {
+ assertThat(
+ EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+ ExerciseType.RUNNING
+ ).supportedGoals
+ ).isEqualTo(
+ EXERCISE_CAPABILITIES.typeToCapabilities.get(ExerciseType.RUNNING)!!.supportedGoals
+ )
+ }
+
+ @Test
+ fun supportedMilestonesForExercise() {
+ assertThat(
+ EXERCISE_CAPABILITIES.getExerciseTypeCapabilities(
+ ExerciseType.RUNNING).supportedMilestones
+ ).isEqualTo(
+ EXERCISE_CAPABILITIES.typeToCapabilities.get(ExerciseType.RUNNING)!!.supportedMilestones
+ )
+ }
+
+ @Test
+ fun exercisesSupportingAutoResumeAndPause_returnCorrectSet() {
+ val supportsAutoPauseAndResume = ExerciseTypeCapabilities(
+ supportedDataTypes = ImmutableSet.of(),
+ supportedGoals = ImmutableMap.of(),
+ supportedMilestones = ImmutableMap.of(),
+ supportsAutoPauseAndResume = true,
+ )
+ val doesNotSupportAutoPauseAndResume = ExerciseTypeCapabilities(
+ supportedDataTypes = ImmutableSet.of(),
+ supportedGoals = ImmutableMap.of(),
+ supportedMilestones = ImmutableMap.of(),
+ supportsAutoPauseAndResume = false,
+ )
+ val exerciseCapabilities = ExerciseCapabilities(
+ ImmutableMap.of(
+ ExerciseType.WALKING, supportsAutoPauseAndResume,
+ ExerciseType.RUNNING, doesNotSupportAutoPauseAndResume
+ )
+ )
+
+ assertThat(exerciseCapabilities.autoPauseAndResumeEnabledExercises)
+ .containsExactly(ExerciseType.WALKING)
+ }
+
+ @Test
+ fun parcelable_roundTrip_returnsOriginalCapabilities() {
+ val proto = EXERCISE_CAPABILITIES.proto
+ val capabilities = ExerciseCapabilities(proto)
+
+ assertThat(capabilities.supportedExerciseTypes).containsExactlyElementsIn(
+ EXERCISE_CAPABILITIES.supportedExerciseTypes
+ )
+ assertThat(capabilities.autoPauseAndResumeEnabledExercises).containsExactlyElementsIn(
+ EXERCISE_CAPABILITIES.autoPauseAndResumeEnabledExercises
+ )
+ }
+
+ @Test
+ fun parcelable_roundTrip_returnsEmptyOriginalCapabilities() {
+ val emptyCapabilities = ExerciseCapabilities(ImmutableMap.of())
+ val roundTripEmptyCapabilities = ExerciseCapabilities(emptyCapabilities.proto)
+
+ assertThat(emptyCapabilities.supportedExerciseTypes).containsExactlyElementsIn(
+ roundTripEmptyCapabilities.supportedExerciseTypes
+ )
+ assertThat(emptyCapabilities.autoPauseAndResumeEnabledExercises).containsExactlyElementsIn(
+ roundTripEmptyCapabilities.autoPauseAndResumeEnabledExercises
+ )
+ }
+
+ companion object {
+ private val WALKING_CAPABILITIES = ExerciseTypeCapabilities(
+ supportedDataTypes = ImmutableSet.of(DataType.STEPS),
+ supportedGoals = ImmutableMap.of(
+ DataType.STEPS_TOTAL,
+ ImmutableSet.of(ComparisonType.GREATER_THAN)
+ ),
+ supportedMilestones = ImmutableMap.of(
+ DataType.STEPS_TOTAL,
+ ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+ ),
+ supportsAutoPauseAndResume = false,
+ )
+ private val RUNNING_CAPABILITIES = ExerciseTypeCapabilities(
+ supportedDataTypes = ImmutableSet.of(DataType.HEART_RATE_BPM, DataType.SPEED),
+ supportedGoals = ImmutableMap.of(
+ DataType.HEART_RATE_BPM_STATS,
+ ImmutableSet.of(ComparisonType.GREATER_THAN, ComparisonType.LESS_THAN),
+ DataType.SPEED_STATS,
+ ImmutableSet.of(ComparisonType.LESS_THAN)
+ ),
+ supportedMilestones = ImmutableMap.of(
+ DataType.HEART_RATE_BPM_STATS,
+ ImmutableSet.of(ComparisonType.GREATER_THAN_OR_EQUAL),
+ DataType.SPEED_STATS,
+ ImmutableSet.of(ComparisonType.LESS_THAN, ComparisonType.GREATER_THAN)
+ ),
+ supportsAutoPauseAndResume = true,
+ )
+
+ private val SWIMMING_CAPABILITIES = ExerciseTypeCapabilities(
+ supportedDataTypes = emptySet(),
+ supportedGoals = emptyMap(),
+ supportedMilestones = emptyMap(),
+ supportsAutoPauseAndResume = true,
+ )
+
+ private val EXERCISE_TYPE_TO_EXERCISE_CAPABILITIES_MAPPING =
+ ImmutableMap.of(
+ ExerciseType.WALKING, WALKING_CAPABILITIES,
+ ExerciseType.RUNNING, RUNNING_CAPABILITIES,
+ ExerciseType.SWIMMING_POOL, SWIMMING_CAPABILITIES
+ )
+
+ private val EXERCISE_CAPABILITIES: ExerciseCapabilities =
+ ExerciseCapabilities(EXERCISE_TYPE_TO_EXERCISE_CAPABILITIES_MAPPING)
+ }
+}
\ No newline at end of file
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/lifecycle/integration-tests/incrementality/build.gradle b/lifecycle/integration-tests/incrementality/build.gradle
index f048361..21dcdf0 100644
--- a/lifecycle/integration-tests/incrementality/build.gradle
+++ b/lifecycle/integration-tests/incrementality/build.gradle
@@ -32,7 +32,11 @@
SdkResourceGenerator.generateForHostTest(project)
// lifecycle-common and annotation are the dependencies of lifecycle-compiler
-tasks.findByPath("test").dependsOn(tasks.findByPath(":lifecycle:lifecycle-compiler:publish"),
- tasks.findByPath(":lifecycle:lifecycle-common:publish"),
- tasks.findByPath(":lifecycle:lifecycle-runtime:publish"),
- tasks.findByPath(":annotation:annotation:publish"))
+tasks.findByPath("test").configure {
+ dependsOn(":lifecycle:lifecycle-compiler:publish")
+ dependsOn(":lifecycle:lifecycle-common:publish")
+ dependsOn(":lifecycle:lifecycle-runtime:publish")
+ dependsOn(":annotation:annotation:publish")
+ dependsOn(":arch:core:core-common:publish")
+ dependsOn(":arch:core:core-runtime:publish")
+}
diff --git a/lifecycle/settings.gradle b/lifecycle/settings.gradle
index cda538c..87799dd 100644
--- a/lifecycle/settings.gradle
+++ b/lifecycle/settings.gradle
@@ -29,6 +29,8 @@
selectProjectsFromAndroidX({ name ->
if (name.startsWith(":lifecycle")) return true
if (name.startsWith(":annotation")) return true
+ if (name.startsWith(":arch:core:core-common")) return true
+ if (name.startsWith(":arch:core:core-runtime")) return true
if (name == ":internal-testutils-runtime") return true
if (name == ":internal-testutils-truth") return true
if (isNeededForComposePlayground(name)) return true
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 8bfb087..742ab63 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -191,7 +191,7 @@
val nowTime = System.nanoTime()
markStateForRemoval(key, states, nowTime)
states.add(
- StateData(
+ getStateData(
nowTime, -1,
StateInfo(key, value)
)
diff --git a/navigation/settings.gradle b/navigation/settings.gradle
index 1850a6f..ad3248f 100644
--- a/navigation/settings.gradle
+++ b/navigation/settings.gradle
@@ -31,7 +31,7 @@
if (name.startsWith(":navigation")) return true
if (name.startsWith(":annotation")) return true
if (name == ":compose:integration-tests:demos:common") return true
- if (name.startsWith(":lifecycle")) return true
+ if (name.startsWith(":lifecycle") && !name.contains("integration-tests")) return true
if (name.startsWith(":savedstate")) return true
if (name == ":internal-testutils-navigation") return true
if (name == ":internal-testutils-runtime") return true
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index b869342..2c7f44a 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -2,10 +2,12 @@
package androidx.paging.testing {
public final class PagerFlowSnapshotKt {
- method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+ method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<java.util.List<Value>>);
}
public final class SnapshotLoader<Value> {
+ method public suspend Object? appendScrollWhile(kotlin.jvm.functions.Function2<Value,? super kotlin.coroutines.Continuation<java.lang.Boolean>,?> predicate, kotlin.coroutines.Continuation<kotlin.Unit>);
+ method public suspend Object? refresh(kotlin.coroutines.Continuation<kotlin.Unit>);
}
public final class StaticListPagingSourceFactoryKt {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
index abf0652..43eea72 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -17,6 +17,7 @@
package androidx.paging.testing
import androidx.paging.DifferCallback
+import androidx.paging.ItemSnapshotList
import androidx.paging.LoadState
import androidx.paging.LoadStates
import androidx.paging.NullPaddedList
@@ -33,7 +34,8 @@
import kotlinx.coroutines.withContext
/**
- * Runs the [SnapshotLoader] load operations that are passed in and returns a List of loaded data.
+ * Runs the [SnapshotLoader] load operations that are passed in and returns a List of data
+ * that would be presented to the UI after all load operations are complete.
*
* @param coroutineScope The [CoroutineScope] to collect from this Flow<PagingData> and contains
* the [CoroutineScope.coroutineContext] to load data from.
@@ -42,20 +44,20 @@
*/
public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
coroutineScope: CoroutineScope,
- loadOperations: suspend SnapshotLoader<Value>.() -> Unit
-): List<Value> {
+ loadOperations: suspend SnapshotLoader<Value>.() -> @JvmSuppressWildcards Unit
+): @JvmSuppressWildcards List<Value> {
lateinit var loader: SnapshotLoader<Value>
val callback = object : DifferCallback {
override fun onChanged(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
override fun onInserted(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
override fun onRemoved(position: Int, count: Int) {
- loader.onDataSetChanged(loader.generation.value)
+ loader.onDataSetChanged(loader.generations.value)
}
}
@@ -68,6 +70,19 @@
onListPresentable: () -> Unit
): Int? {
onListPresentable()
+ /**
+ * On new generation, SnapshotLoader needs the latest [ItemSnapshotList]
+ * index last updated by the initial refresh so that it can
+ * prepend/append from then on based on that index.
+ *
+ * This last updated index is necessary because initial load
+ * key may not be 0, for example when [Pager].initialKey != 0
+ *
+ * Any subsequent SnapshotLoader loads are based on the index tracked by
+ * [SnapshotLoader] internally.
+ */
+ val lastLoadedIndex = snapshot().placeholdersBefore + snapshot().items.size - 1
+ loader.generations.value.lastAccessedIndex.set(lastLoadedIndex)
return null
}
}
@@ -81,7 +96,7 @@
*/
val job = coroutineScope.launch {
this@asSnapshot.collectLatest {
- // TODO increase generation count
+ incrementGeneration(loader)
differ.collectFrom(it)
}
}
@@ -90,14 +105,16 @@
* Runs the input [loadOperations].
*
* Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
- * cancels the collection on this [Pager.flow] after [loadOperations] completes.
+ * cancels the collection on this [Pager.flow] after [loadOperations] completes and Paging
+ * is idle.
*
* Returns a List of loaded data.
*/
return withContext(coroutineScope.coroutineContext) {
differ.awaitNotLoading()
-
loader.loadOperations()
+ differ.awaitNotLoading()
+
job.cancelAndJoin()
differ.snapshot().items
@@ -118,4 +135,13 @@
private fun LoadStates.isIdle(): Boolean {
return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
prepend is LoadState.NotLoading
+}
+
+private fun <Value : Any> incrementGeneration(loader: SnapshotLoader<Value>) {
+ val currGen = loader.generations.value
+ if (currGen.id == loader.generations.value.id) {
+ loader.generations.value = Generation(
+ id = currGen.id + 1
+ )
+ }
}
\ No newline at end of file
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
index 56cce09..86c1b61 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
@@ -17,8 +17,16 @@
package androidx.paging.testing
import androidx.paging.DifferCallback
+import androidx.paging.PagingData
import androidx.paging.PagingDataDiffer
+import androidx.paging.PagingSource
+import androidx.paging.LoadType.APPEND
+import androidx.paging.PagingConfig
+import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapLatest
/**
* Contains the public APIs for load operations in tests.
@@ -29,28 +37,122 @@
public class SnapshotLoader<Value : Any> internal constructor(
private val differ: PagingDataDiffer<Value>
) {
- internal val generation = MutableStateFlow(Generation(0))
+ internal val generations = MutableStateFlow(Generation())
- // TODO add public loading APIs such as scrollTo(index)
+ /**
+ * Refresh the data that is presented on the UI.
+ *
+ * [refresh] triggers a new generation of [PagingData] / [PagingSource]
+ * to represent an updated snapshot of the backing dataset.
+ *
+ * This fake paging operation mimics UI-driven refresh signals such as swipe-to-refresh.
+ */
+ public suspend fun refresh(): @JvmSuppressWildcards Unit {
+ differ.awaitNotLoading()
+ differ.refresh()
+ differ.awaitNotLoading()
+ }
- // the callback to be invoked by DifferCallback on a single generation
- // increase the callbackCount to notify SnapshotLoader that the dataset has updated
+ /**
+ * Imitates scrolling down paged items, [appending][APPEND] data until the given
+ * predicate returns false.
+ *
+ * Note: This API loads an item before passing it into the predicate. This means the
+ * loaded pages may include the page which contains the item that does not match the
+ * predicate. For example, if pageSize = 2, the predicate
+ * {item: Int -> item < 3 } will return items [[1, 2],[3, 4]] where [3, 4] is the page
+ * containing the boundary item[3] not matching the predicate.
+ *
+ * The loaded pages are also dependent on [PagingConfig] settings such as
+ * [PagingConfig.prefetchDistance]:
+ * - if `prefetchDistance` > 0, the resulting appends will include prefetched items.
+ * For example, if pageSize = 2 and prefetchDistance = 2, the predicate
+ * {item: Int -> item < 3 } will load items [[1, 2], [3, 4], [5, 6]] where [5, 6] is the
+ * prefetched page.
+ *
+ * @param [predicate] the predicate to match (return true) to continue append scrolls
+ */
+ public suspend fun appendScrollWhile(
+ predicate: suspend (item: @JvmSuppressWildcards Value) -> @JvmSuppressWildcards Boolean
+ ): @JvmSuppressWildcards Unit {
+ differ.awaitNotLoading()
+ appendOrPrepend(LoadType.APPEND, predicate)
+ differ.awaitNotLoading()
+ }
+
+ private suspend fun appendOrPrepend(
+ loadType: LoadType,
+ predicate: suspend (item: Value) -> Boolean
+ ) {
+ do {
+ // Get and update the index to load from. Return if index is invalid.
+ val index = nextLoadIndexOrNull(loadType) ?: return
+ val item = loadItem(index)
+ } while (predicate(item))
+ }
+
+ /**
+ * Get and update the index to load from. Returns null if next index is out of bounds.
+ *
+ * This method is responsible for updating the [Generation.lastAccessedIndex] that is then sent
+ * to the differ to trigger load for that index.
+ */
+ private fun nextLoadIndexOrNull(loadType: LoadType): Int? {
+ val currGen = generations.value
+ return when (loadType) {
+ LoadType.PREPEND -> {
+ if (currGen.lastAccessedIndex.get() <= 0) {
+ return null
+ }
+ currGen.lastAccessedIndex.decrementAndGet()
+ }
+ LoadType.APPEND -> {
+ if (currGen.lastAccessedIndex.get() >= differ.size - 1) {
+ return null
+ }
+ currGen.lastAccessedIndex.incrementAndGet()
+ }
+ }
+ }
+
+ // Executes actual loading by accessing the PagingDataDiffer
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ private suspend fun loadItem(index: Int): Value {
+ differ[index]
+
+ // awaits for the item to be loaded
+ return generations.mapLatest {
+ differ.peek(index)
+ }.filterNotNull().first()
+ }
+
+ /**
+ * The callback to be invoked by DifferCallback on a single generation.
+ * Increase the callbackCount to notify SnapshotLoader that the dataset has updated
+ */
internal fun onDataSetChanged(gen: Generation) {
- val currGen = generation.value
+ val currGen = generations.value
// we make sure the generation with the dataset change is still valid because we
// want to disregard callbacks on stale generations
if (gen.id == currGen.id) {
- generation.value = gen.copy(
+ generations.value = gen.copy(
callbackCount = currGen.callbackCount + 1
)
}
}
+
+ private enum class LoadType {
+ PREPEND,
+ APPEND
+ }
}
internal data class Generation(
- // Id of the current Paging generation. Incremented on each new generation (when a new
- // PagingData is received).
- val id: Int,
+ /**
+ * Id of the current Paging generation. Incremented on each new generation (when a new
+ * PagingData is received).
+ */
+ val id: Int = -1,
/**
* A count of the number of times Paging invokes a [DifferCallback] callback within a single
@@ -59,5 +161,10 @@
* The callbackCount enables [SnapshotLoader] to await for a requested item and continue
* loading next item only after a callback is invoked.
*/
- val callbackCount: Int = 0
+ val callbackCount: Int = 0,
+
+ /**
+ * Tracks the last accessed index on the differ for this generation
+ */
+ var lastAccessedIndex: AtomicInteger = AtomicInteger()
)
\ No newline at end of file
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 9304ec8..89a711a 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -18,9 +18,13 @@
import androidx.paging.Pager
import androidx.paging.PagingConfig
+import androidx.paging.cachedIn
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -43,15 +47,29 @@
}
@Test
- fun simpleInitialRefresh() {
+ fun initialRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+ // first page + prefetched page
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7)
+ )
+ }
+ }
+
+ @Test
+ fun initialRefresh_withoutPrefetch() {
val dataFlow = flowOf(List(30) { it })
val factory = dataFlow.asPagingSourceFactory(testScope)
val pager = Pager(
- config = PagingConfig(
- pageSize = 3,
- initialLoadSize = 5,
- prefetchDistance = 0
- ),
+ config = CONFIG_NO_PREFETCH,
pagingSourceFactory = factory
)
testScope.runTest {
@@ -64,21 +82,408 @@
}
@Test
- fun emptyInitialRefresh() {
- val dataFlow = emptyFlow<List<Int>>()
+ fun initialRefresh_withInitialKey() {
+ val dataFlow = flowOf(List(30) { it })
val factory = dataFlow.asPagingSourceFactory(testScope)
val pager = Pager(
- config = PagingConfig(
- pageSize = 3,
- initialLoadSize = 5,
- prefetchDistance = 0
- ),
+ config = CONFIG,
+ initialKey = 10,
pagingSourceFactory = factory
)
testScope.runTest {
val snapshot = pager.flow.asSnapshot(this) {}
- assertThat(snapshot).isEmpty()
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)
+ )
}
}
+
+ @Test
+ fun initialRefresh_withInitialKey_withoutPrefetch() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14)
+ )
+ }
+ }
+
+ @Test
+ fun emptyInitialRefresh() {
+ val dataFlow = emptyFlow<List<Int>>()
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {}
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+ }
+ }
+
+ @Test
+ fun manualRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = factory
+ ).flow.cachedIn(testScope.backgroundScope)
+
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ refresh()
+ }
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4),
+ )
+ }
+ }
+
+ @Test
+ fun manualEmptyRefresh() {
+ val dataFlow = emptyFlow<List<Int>>()
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = factory
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ refresh()
+ }
+ assertThat(snapshot).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+ }
+ }
+
+ @Test
+ fun append() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 7
+ }
+ }
+
+ // includes initial load, 1st page, 2nd page (from prefetch)
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+ )
+ }
+ }
+
+ @Test
+ fun append_withInitialKey() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 10,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 18
+ }
+ }
+
+ // items[7-23]
+ // extra prepended page from prefetch after initial refresh
+ // extra appended page from prefetch after append
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(17) { it + 7 }
+ )
+ }
+ }
+
+ @Test
+ fun append_withInitialKey_withoutPrefetch() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 18
+ }
+ }
+
+ // items[10-20]
+ // although no prefetch, extra appended page because paging loaded item 18
+ // and its entire page before the predicate returned false
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(11) { it + 10 }
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveAppend() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 7
+ }
+ }
+
+ val snapshot2 = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 22
+ }
+ }
+
+ // includes initial load, 1st page, 2nd page (from prefetch)
+ assertThat(snapshot1).containsExactlyElementsIn(
+ List(11) { it }
+ )
+
+ // includes extra page from prefetch
+ assertThat(snapshot2).containsExactlyElementsIn(
+ List(26) { it }
+ )
+ }
+ }
+
+ @Test
+ fun append_outOfBounds_returnsCurrentlyLoadedItems() {
+ val dataFlow = flowOf(List(10) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ )
+ testScope.runTest {
+ val snapshot = pager.flow.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ // condition scrolls till end of data since we only have 10 items
+ item < 18
+ }
+ }
+
+ // returns the items loaded before index becomes out of bounds
+ assertThat(snapshot).containsExactlyElementsIn(
+ List(10) { it }
+ )
+ }
+ }
+
+ @Test
+ fun refreshAndAppend() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ refresh() // triggers second gen
+ appendScrollWhile { item: Int ->
+ item < 10
+ }
+ }
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+ )
+ }
+ }
+
+ @Test
+ fun appendAndRefresh() {
+ val dataFlow = flowOf(List(30) { it })
+ val factory = dataFlow.asPagingSourceFactory(testScope)
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = factory,
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot = pager.asSnapshot(this) {
+ appendScrollWhile { item: Int ->
+ item < 10
+ }
+ refresh()
+ }
+
+ assertThat(snapshot).containsExactlyElementsIn(
+ // second gen initial load, anchorPos = 10, refreshKey = 8
+ listOf(5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_fromSharedFlow() {
+ val dataFlow = MutableSharedFlow<List<Int>>()
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ dataFlow.emit(List(30) { it })
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+
+ val snapshot3 = pager.asSnapshot(this) {
+ dataFlow.emit(List(30) { it + 30 })
+ }
+ assertThat(snapshot3).containsExactlyElementsIn(
+ listOf(30, 31, 32, 33, 34)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_fromFlow() {
+ val dataFlow = flow {
+ // first gen
+ emit(emptyList())
+ delay(500)
+ // second gen
+ emit(List(30) { it })
+ delay(500)
+ // third gen
+ emit(List(30) { it + 30 })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ emptyList<Int>()
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+
+ val snapshot3 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ assertThat(snapshot3).containsExactlyElementsIn(
+ listOf(30, 31, 32, 33, 34)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_withInitialKey_nullRefreshKey() {
+ val dataFlow = flow {
+ // first gen
+ emit(List(20) { it })
+ delay(500)
+ // second gen
+ emit(List(20) { it })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) { }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14)
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ // wait for second gen to complete
+ delay(500)
+ }
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(0, 1, 2, 3, 4)
+ )
+ }
+ }
+
+ @Test
+ fun consecutiveGenerations_withInitialKey_nonNullRefreshKey() {
+ val dataFlow = flow {
+ // first gen
+ emit(List(20) { it })
+ delay(500)
+ // second gen
+ emit(List(20) { it })
+ }
+ val pager = Pager(
+ config = CONFIG_NO_PREFETCH,
+ initialKey = 10,
+ pagingSourceFactory = dataFlow.asPagingSourceFactory(testScope.backgroundScope)
+ ).flow.cachedIn(testScope.backgroundScope)
+ testScope.runTest {
+ val snapshot1 = pager.asSnapshot(this) {
+ // we scroll to register a non-null anchorPos
+ appendScrollWhile { item: Int ->
+ item < 15
+ }
+ }
+ assertThat(snapshot1).containsExactlyElementsIn(
+ listOf(10, 11, 12, 13, 14, 15, 16, 17)
+ )
+
+ val snapshot2 = pager.asSnapshot(this) {
+ delay(500)
+ }
+ // anchorPos = 15, refreshKey = 13
+ assertThat(snapshot2).containsExactlyElementsIn(
+ listOf(13, 14, 15, 16, 17)
+ )
+ }
+ }
+
+ val CONFIG = PagingConfig(
+ pageSize = 3,
+ initialLoadSize = 5,
+ )
+
+ val CONFIG_NO_PREFETCH = PagingConfig(
+ pageSize = 3,
+ initialLoadSize = 5,
+ prefetchDistance = 0
+ )
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
index f684502..4606a745 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompiler.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.tools.apicompiler
+import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.apicompiler.generator.SandboxApiVersion
import androidx.privacysandbox.tools.apicompiler.generator.SdkCodeGenerator
import androidx.privacysandbox.tools.apicompiler.parser.ApiParser
@@ -35,16 +36,19 @@
) : SymbolProcessor {
companion object {
const val AIDL_COMPILER_PATH_OPTIONS_KEY = "aidl_compiler_path"
- const val USE_COMPAT_LIBRARY_OPTIONS_KEY = "use_sdk_runtime_compat_library"
+ const val SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY = "skip_sdk_runtime_compat_library"
}
- var invoked = false
-
override fun process(resolver: Resolver): List<KSAnnotated> {
- if (invoked) {
+ // This method is called multiple times during compilation and the resolver will only return
+ // relevant files for each particular processing round. This instance might also be kept
+ // by KSP between rounds or for incremental compilation. This means that at some point
+ // KSP will always invoke this processor with no valid services, so we should just stop
+ // processing.
+ if (resolver.getSymbolsWithAnnotation(
+ PrivacySandboxService::class.qualifiedName!!).none()) {
return emptyList()
}
- invoked = true
val path = options[AIDL_COMPILER_PATH_OPTIONS_KEY]?.let(Paths::get)
if (path == null) {
@@ -52,9 +56,11 @@
return emptyList()
}
- val target = if (options[USE_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase() == "true") {
- SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
- } else SandboxApiVersion.API_33
+ val skipCompatLibrary =
+ options[SKIP_SDK_RUNTIME_COMPAT_LIBRARY_OPTIONS_KEY]?.lowercase().toBoolean()
+ val target = if (skipCompatLibrary) {
+ SandboxApiVersion.API_33
+ } else SandboxApiVersion.SDK_RUNTIME_COMPAT_LIBRARY
val parsedApi = ApiParser(resolver, logger).parseApi()
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/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index bc189a4..78e3098 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -75,7 +75,7 @@
// Sources created by the AIDL compiler have to be copied to files created
// through the KSP APIs, so that they are included in downstream compilation.
val kspGeneratedFile = codeGenerator.createNewFile(
- Dependencies(false),
+ Dependencies.ALL_FILES,
source.packageName,
source.interfaceName,
extensionName = "java"
@@ -113,7 +113,7 @@
private fun generateToolMetadata() {
codeGenerator.createNewFile(
- Dependencies(false),
+ Dependencies.ALL_FILES,
Metadata.filePath.parent.toString(),
Metadata.filePath.nameWithoutExtension,
Metadata.filePath.extension,
@@ -138,8 +138,7 @@
}
private fun write(spec: FileSpec) {
-
- codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
+ codeGenerator.createNewFile(Dependencies.ALL_FILES, spec.packageName, spec.name)
.bufferedWriter().use(spec::writeTo)
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
index 23fb1b7..5782e08 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ApiParser.kt
@@ -39,8 +39,9 @@
/** Top-level entry point to parse a complete user-defined sandbox SDK API into a [ParsedApi]. */
class ApiParser(private val resolver: Resolver, private val logger: KSPLogger) {
- private val interfaceParser = InterfaceParser(logger)
- private val valueParser = ValueParser(logger)
+ private val typeParser = TypeParser(logger)
+ private val interfaceParser = InterfaceParser(logger, typeParser)
+ private val valueParser = ValueParser(logger, typeParser)
fun parseApi(): ParsedApi {
val services = parseAllServices()
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
deleted file mode 100644
index 2c0f555..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/Converters.kt
+++ /dev/null
@@ -1,29 +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.privacysandbox.tools.apicompiler.parser
-
-import androidx.privacysandbox.tools.core.model.Type
-import com.google.devtools.ksp.symbol.KSDeclaration
-
-internal object Converters {
- fun typeFromDeclaration(declaration: KSDeclaration): Type {
- return Type(
- packageName = declaration.packageName.getFullName(),
- simpleName = declaration.simpleName.getShortName(),
- )
- }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
index 79c69c9..cb2b9d0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParser.kt
@@ -19,7 +19,6 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.Parameter
-import androidx.privacysandbox.tools.core.model.Type
import com.google.devtools.ksp.getDeclaredFunctions
import com.google.devtools.ksp.getDeclaredProperties
import com.google.devtools.ksp.isPublic
@@ -27,12 +26,10 @@
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
-import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.KSValueParameter
import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
-internal class InterfaceParser(private val logger: KSPLogger) {
+internal class InterfaceParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
private val validInterfaceModifiers = setOf(Modifier.PUBLIC)
private val validMethodModifiers = setOf(Modifier.PUBLIC, Modifier.SUSPEND)
@@ -47,13 +44,15 @@
}
if (interfaceDeclaration.getDeclaredProperties().any()) {
logger.error(
- "Error in $name: annotated interfaces cannot declare properties.")
+ "Error in $name: annotated interfaces cannot declare properties."
+ )
}
if (interfaceDeclaration.declarations.filterIsInstance<KSClassDeclaration>()
.any(KSClassDeclaration::isCompanionObject)
) {
logger.error(
- "Error in $name: annotated interfaces cannot declare companion objects.")
+ "Error in $name: annotated interfaces cannot declare companion objects."
+ )
}
val invalidModifiers =
interfaceDeclaration.modifiers.filterNot(validInterfaceModifiers::contains)
@@ -75,7 +74,7 @@
val methods = interfaceDeclaration.getDeclaredFunctions().map(::parseMethod).toList()
return AnnotatedInterface(
- type = Converters.typeFromDeclaration(interfaceDeclaration),
+ type = typeParser.parseFromDeclaration(interfaceDeclaration),
methods = methods,
)
}
@@ -102,7 +101,7 @@
if (method.returnType == null) {
logger.error("Error in $name: failed to resolve return type.")
}
- val returnType = parseType(method, method.returnType!!.resolve())
+ val returnType = typeParser.parseFromTypeReference(method.returnType!!, name)
val parameters =
method.parameters.map { parameter -> parseParameter(method, parameter) }.toList()
@@ -128,15 +127,7 @@
return Parameter(
name = parameter.name!!.getFullName(),
- type = parseType(method, parameter.type.resolve()),
+ type = typeParser.parseFromTypeReference(parameter.type, name),
)
}
-
- private fun parseType(method: KSFunctionDeclaration, type: KSType): Type {
- val name = method.qualifiedName?.getFullName() ?: method.simpleName.getFullName()
- if (type.nullability == Nullability.NULLABLE) {
- logger.error("Error in $name: nullable types are not supported.")
- }
- return Converters.typeFromDeclaration(type.declaration)
- }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
new file mode 100644
index 0000000..38a96b6
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/TypeParser.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.privacysandbox.tools.apicompiler.parser
+
+import androidx.privacysandbox.tools.core.model.Type
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Nullability
+import com.google.devtools.ksp.symbol.Variance
+
+internal class TypeParser(private val logger: KSPLogger) {
+ fun parseFromDeclaration(declaration: KSDeclaration): Type {
+ return Type(
+ packageName = declaration.packageName.getFullName(),
+ simpleName = declaration.simpleName.getShortName(),
+ )
+ }
+
+ fun parseFromTypeReference(typeReference: KSTypeReference, debugName: String): Type {
+ val resolvedType = typeReference.resolve()
+ if (resolvedType.isError) {
+ logger.error("Failed to resolve type for $debugName.")
+ }
+ if (resolvedType.nullability == Nullability.NULLABLE) {
+ logger.error("Error in $debugName: nullable types are not supported.")
+ }
+ val typeArguments = typeReference.element?.typeArguments?.mapNotNull {
+ if (it.type == null) {
+ logger.error("Error in $debugName: null type argument.")
+ }
+ if (it.variance != Variance.INVARIANT) {
+ logger.error("Error in $debugName: only invariant type arguments are supported.")
+ }
+ it.type
+ } ?: emptyList()
+ return Type(
+ packageName = resolvedType.declaration.packageName.getFullName(),
+ simpleName = resolvedType.declaration.simpleName.getShortName(),
+ typeParameters = typeArguments.map { parseFromTypeReference(it, debugName) },
+ )
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
index 39d8a26..64f9c6b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParser.kt
@@ -25,13 +25,13 @@
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.google.devtools.ksp.symbol.Modifier
-import com.google.devtools.ksp.symbol.Nullability
-internal class ValueParser(private val logger: KSPLogger) {
+internal class ValueParser(private val logger: KSPLogger, private val typeParser: TypeParser) {
fun parseValue(value: KSAnnotated): AnnotatedValue? {
if (value !is KSClassDeclaration ||
value.classKind != ClassKind.CLASS ||
- !value.modifiers.contains(Modifier.DATA)) {
+ !value.modifiers.contains(Modifier.DATA)
+ ) {
logger.error(
"Only data classes can be annotated with @PrivacySandboxValue."
)
@@ -61,7 +61,7 @@
}
return AnnotatedValue(
- type = Converters.typeFromDeclaration(value),
+ type = typeParser.parseFromDeclaration(value),
properties = value.getAllProperties().map(::parseProperty).toList()
)
}
@@ -71,17 +71,9 @@
if (property.isMutable) {
logger.error("Error in $name: properties cannot be mutable.")
}
- val type = property.type.resolve()
- if (type.isError) {
- logger.error("Failed to resolve type for property $name.")
- }
- if (type.nullability == Nullability.NULLABLE) {
- logger.error("Error in $name: nullable types are not supported.")
- }
-
return ValueProperty(
name = property.simpleName.getShortName(),
- type = Converters.typeFromDeclaration(type.declaration),
+ type = typeParser.parseFromTypeReference(property.type, name),
)
}
}
\ 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 78f636c..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
@@ -34,7 +34,11 @@
val inputSources = loadSourcesFromDirectory(inputTestDataDir)
val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
- val result = compileWithPrivacySandboxKspCompiler(inputSources)
+ val result = compileWithPrivacySandboxKspCompiler(
+ inputSources,
+ platformStubs = PlatformStubs.API_33,
+ extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+ )
assertThat(result).succeeds()
val expectedAidlFilepath = listOf(
@@ -48,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/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
index cda0661..1ef27b0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkRuntimeLibrarySdkTest.kt
@@ -32,11 +32,7 @@
val inputSources = loadSourcesFromDirectory(inputTestDataDir)
val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
- val result = compileWithPrivacySandboxKspCompiler(
- inputSources,
- platformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
- extraProcessorOptions = mapOf("use_sdk_runtime_compat_library" to "true")
- )
+ val result = compileWithPrivacySandboxKspCompiler(inputSources)
assertThat(result).succeeds()
val expectedAidlFilepath = listOf(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index 5de917b..e6b6e3d 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -28,7 +28,7 @@
*/
fun compileWithPrivacySandboxKspCompiler(
sources: List<Source>,
- platformStubs: PlatformStubs = PlatformStubs.API_33,
+ platformStubs: PlatformStubs = PlatformStubs.SDK_RUNTIME_LIBRARY,
extraProcessorOptions: Map<String, String> = mapOf(),
): TestCompilationResult {
val provider = PrivacySandboxKspCompiler.Provider()
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
index afda28e..f37b09bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/InterfaceParserTest.kt
@@ -43,6 +43,7 @@
@PrivacySandboxService
interface MySdk {
suspend fun doStuff(x: Int, y: Int): String
+ suspend fun processList(list: List<Int>): List<String>
fun doMoreStuff()
}
""",
@@ -66,6 +67,17 @@
),
returnType = Types.string,
isSuspend = true,
+ ),
+ Method(
+ name = "processList",
+ parameters = listOf(
+ Parameter(
+ name = "list",
+ type = Types.list(Types.int),
+ )
+ ),
+ returnType = Types.list(Types.string),
+ isSuspend = true,
), Method(
name = "doMoreStuff",
parameters = listOf(),
@@ -220,9 +232,18 @@
fun parameterWithGenerics_fails() {
checkSourceFails(serviceMethod("suspend fun foo(x: MutableList<Int>)"))
.containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
- "or @PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
+ )
+ }
+
+ @Test
+ fun listParameterWithNonInvariantTypeArgument_fails() {
+ checkSourceFails(serviceMethod("suspend fun foo(x: List<in Int>)"))
+ .containsExactlyErrors(
+ "Error in com.mysdk.MySdk.foo: only invariant type arguments are supported."
)
}
@@ -230,9 +251,10 @@
fun parameterLambda_fails() {
checkSourceFails(serviceMethod("suspend fun foo(x: (Int) -> Int)"))
.containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback " +
- "or @PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
)
}
@@ -251,7 +273,7 @@
"""
)
checkSourceFails(source).containsExactlyErrors(
- "Error in com.mysdk.MySdk.foo: only primitives, data classes annotated with " +
+ "Error in com.mysdk.MySdk.foo: only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
"supported as return types."
)
@@ -361,12 +383,14 @@
name = "request",
type = Type(
packageName = "com.mysdk",
- simpleName = "MyInterface"),
+ simpleName = "MyInterface"
+ ),
)
),
returnType = Type(
packageName = "com.mysdk",
- simpleName = "MyInterface"),
+ simpleName = "MyInterface"
+ ),
isSuspend = true,
),
)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index 5dd00bf..132a95a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -52,7 +52,7 @@
data class MySdkResponse(
val magicPayload: MagicPayload, val isTrulyMagic: Boolean)
@PrivacySandboxValue
- data class MagicPayload(val magicNumber: Long)
+ data class MagicPayload(val magicList: List<Long>)
"""
)
assertThat(parseSources(source)).isEqualTo(
@@ -92,9 +92,7 @@
),
AnnotatedValue(
type = Type(packageName = "com.mysdk", simpleName = "MagicPayload"),
- properties = listOf(
- ValueProperty("magicNumber", Types.long),
- )
+ properties = listOf(ValueProperty("magicList", Types.list(Types.long)))
),
)
)
@@ -178,7 +176,7 @@
)
checkSourceFails(dataClass)
.containsExactlyErrors(
- "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+ "Error in com.mysdk.MySdkRequest.foo: only primitives, lists, data classes " +
"annotated with @PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as properties."
)
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/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 1db70b6..e588bbd 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -109,10 +109,14 @@
if (classifier !is KmClassifier.Class) {
throw PrivacySandboxParsingException("Unsupported type in API description: $type")
}
- return parseClassName(classifier.name)
+ val typeArguments = type.arguments.map { parseType(it.type!!) }
+ return parseClassName(classifier.name, typeArguments)
}
- private fun parseClassName(className: ClassName): Type {
+ private fun parseClassName(
+ className: ClassName,
+ typeArguments: List<Type> = emptyList()
+ ): Type {
// Package names are separated with slashes and nested classes are separated with dots.
// (e.g com/example/OuterClass.InnerClass).
val (packageName, simpleName) = className.split('/').run {
@@ -126,7 +130,7 @@
)
}
- return Type(packageName, simpleName)
+ return Type(packageName, simpleName, typeArguments)
}
private fun validate(api: ParsedApi) {
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/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index cc006e8..bb3c24f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -49,6 +49,7 @@
fun doSomething(magicNumber: Int, awesomeString: String)
suspend fun getPayload(request: PayloadRequest): PayloadResponse
suspend fun getInterface(): MyInterface
+ suspend fun processList(list: List<Long>): List<Long>
}
@PrivacySandboxInterface
interface MyInterface {
@@ -64,7 +65,7 @@
interface CustomCallback {
fun onComplete(status: Int)
}
- """
+ """,
)
val expectedPayloadType = AnnotatedValue(
@@ -118,7 +119,18 @@
),
returnType = expectedPayloadResponse.type,
isSuspend = true,
- )
+ ),
+ Method(
+ name = "processList",
+ parameters = listOf(
+ Parameter(
+ "list",
+ Types.list(Types.long)
+ )
+ ),
+ returnType = Types.list(Types.long),
+ isSuspend = true,
+ ),
)
)
val expectedInterface =
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/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index 467d8ac..0ee111d 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -41,4 +41,9 @@
val char = Type(packageName = "kotlin", simpleName = "Char")
val short = Type(packageName = "kotlin", simpleName = "Short")
val primitiveTypes = setOf(unit, boolean, int, long, float, double, string, char, short)
+ fun list(elementType: Type) = Type(
+ packageName = "kotlin.collections",
+ simpleName = "List",
+ typeParameters = listOf(elementType)
+ )
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
index 71e5cd2..3442e0f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Type.kt
@@ -19,6 +19,7 @@
data class Type(
val packageName: String,
val simpleName: String,
+ val typeParameters: List<Type> = emptyList(),
) {
val qualifiedName = "$packageName.$simpleName"
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index 0f6e3f8..3b5f012 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -19,9 +19,14 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.AnnotatedValue
import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Type
import androidx.privacysandbox.tools.core.model.Types
class ModelValidator private constructor(val api: ParsedApi) {
+ private val values = api.values.map(AnnotatedValue::type)
+ private val interfaces = api.interfaces.map(AnnotatedInterface::type)
+ private val callbacks = api.callbacks.map(AnnotatedInterface::type)
+
private val errors: MutableList<String> = mutableListOf()
companion object {
@@ -61,33 +66,24 @@
}
private fun validateServiceAndInterfaceMethods() {
- val allowedParameterTypes =
- (api.values.map(AnnotatedValue::type) +
- api.callbacks.map(AnnotatedInterface::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
- val allowedReturnValueTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
val annotatedInterfaces = api.services + api.interfaces
for (annotatedInterface in annotatedInterfaces) {
for (method in annotatedInterface.methods) {
- if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+ if (method.parameters.any { !(isValidInterfaceParameterType(it.type)) }) {
errors.add(
"Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with @PrivacySandboxValue " +
- "and interfaces annotated with @PrivacySandboxCallback or " +
- "@PrivacySandboxInterface are supported as parameter types."
+ "only primitives, lists, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported " +
+ "as parameter types."
)
}
- if (!allowedReturnValueTypes.contains(method.returnType)) {
+ if (!isValidInterfaceReturnType(method.returnType)) {
errors.add(
"Error in ${annotatedInterface.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with @PrivacySandboxValue " +
- "and interfaces annotated with @PrivacySandboxInterface are " +
- "supported as return types."
+ "only primitives, lists, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as return types."
)
}
}
@@ -95,17 +91,12 @@
}
private fun validateValuePropertyTypes() {
- val allowedValuePropertyTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
for (value in api.values) {
for (property in value.properties) {
- if (!allowedValuePropertyTypes.contains(property.type)) {
+ if (!isValidValuePropertyType(property.type)) {
errors.add(
"Error in ${value.type.qualifiedName}.${property.name}: " +
- "only primitives, data classes annotated with " +
+ "only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as properties."
)
@@ -115,17 +106,12 @@
}
private fun validateCallbackMethods() {
- val allowedParameterTypes =
- (api.values.map(AnnotatedValue::type) +
- api.interfaces.map(AnnotatedInterface::type) +
- Types.primitiveTypes).toSet()
-
for (callback in api.callbacks) {
for (method in callback.methods) {
- if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
+ if (method.parameters.any { !isValidCallbackParameterType(it.type) }) {
errors.add(
"Error in ${callback.type.qualifiedName}.${method.name}: " +
- "only primitives, data classes annotated with " +
+ "only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as callback parameter types."
)
@@ -139,6 +125,29 @@
}
}
}
+
+ private fun isValidInterfaceParameterType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type) || isCallback(type)
+ private fun isValidInterfaceReturnType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+ private fun isValidValuePropertyType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+ private fun isValidCallbackParameterType(type: Type) =
+ isValue(type) || isInterface(type) || isPrimitive(type) || isList(type)
+
+ private fun isValue(type: Type) = values.contains(type)
+ private fun isInterface(type: Type) = interfaces.contains(type)
+ private fun isCallback(type: Type) = callbacks.contains(type)
+ private fun isPrimitive(type: Type) = Types.primitiveTypes.contains(type)
+ private fun isList(type: Type): Boolean {
+ if (type.qualifiedName == "kotlin.collections.List") {
+ require(type.typeParameters.size == 1) {
+ "List type should have one type parameter, found ${type.typeParameters}."
+ }
+ return type.typeParameters[0].let { isValue(it) || isPrimitive(it) }
+ }
+ return false
+ }
}
data class ValidationResult(val errors: List<String>) {
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/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index c3a654d..d13fd0e 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -239,12 +239,45 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdk.returnFoo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface are " +
- "supported as return types.",
- "Error in com.mysdk.MySdk.receiveFoo: only primitives, data classes annotated with " +
- "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxCallback or " +
- "@PrivacySandboxInterface are supported as parameter types."
+ "Error in com.mysdk.MySdk.returnFoo: only primitives, lists, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as return types.",
+ "Error in com.mysdk.MySdk.receiveFoo: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as parameter " +
+ "types."
+ )
+ }
+
+ @Test
+ fun nestedList_throws() {
+ val api = ParsedApi(
+ services = setOf(
+ AnnotatedInterface(
+ type = Type(packageName = "com.mysdk", simpleName = "MySdk"),
+ methods = listOf(
+ Method(
+ name = "processNestedList",
+ parameters = listOf(
+ Parameter(
+ name = "foo",
+ type = Types.list(Types.list(Types.int))
+ )
+ ),
+ returnType = Types.unit,
+ isSuspend = true,
+ ),
+ ),
+ ),
+ )
+ )
+ val validationResult = ModelValidator.validate(api)
+ assertThat(validationResult.isFailure).isTrue()
+ assertThat(validationResult.errors).containsExactly(
+ "Error in com.mysdk.MySdk.processNestedList: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxCallback or @PrivacySandboxInterface are supported as " +
+ "parameter types."
)
}
@@ -266,7 +299,7 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+ "Error in com.mysdk.Foo.bar: only primitives, lists, data classes annotated with " +
"@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
"are supported as properties."
)
@@ -333,8 +366,8 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
- "with @PrivacySandboxValue and interfaces annotated with " +
+ "Error in com.mysdk.MySdkCallback.foo: only primitives, lists, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
"@PrivacySandboxInterface are supported as callback parameter types."
)
}
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-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
index 227ba42..533b018 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XFiler.kt
@@ -22,6 +22,8 @@
import androidx.room.compiler.codegen.kotlin.KotlinTypeSpec
import com.squareup.javapoet.JavaFile
import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
/**
* Code generation interface for XProcessing.
@@ -33,6 +35,20 @@
fun write(fileSpec: FileSpec, mode: Mode = Mode.Isolating)
/**
+ * Writes a resource file that will be part of the output artifact (e.g. jar).
+ *
+ * Only non-source files should be written via this function, if the file path corresponds to a
+ * source file, `.java` or `.kt` this function will throw an exception.
+ *
+ * @return the output stream to write the resource file.
+ */
+ fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: Mode = Mode.Isolating
+ ): OutputStream
+
+ /**
* Specifies whether a file represents aggregating or isolating inputs for incremental
* build purposes. This does not apply in Javac processing because aggregating vs isolating
* is set on the processor level. For more on KSP's definitions of isolating vs aggregating
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
index d61dc26c..93578c7 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacFiler.kt
@@ -16,11 +16,16 @@
package androidx.room.compiler.processing.javac
+import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XProcessingEnv
import com.squareup.javapoet.JavaFile
import com.squareup.kotlinpoet.FileSpec
+import java.io.OutputStream
+import java.nio.file.Path
import javax.annotation.processing.Filer
+import javax.tools.StandardLocation
+import kotlin.io.path.extension
internal class JavacFiler(
private val processingEnv: XProcessingEnv,
@@ -40,4 +45,24 @@
}
fileSpec.writeTo(delegate)
}
+
+ override fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: XFiler.Mode
+ ): OutputStream {
+ require(filePath.extension != "java" && filePath.extension != "kt") {
+ "Could not create resource file with a source type extension. File must not be " +
+ "neither '.java' nor '.kt', but was: $filePath"
+ }
+ val javaOriginatingElements =
+ originatingElements.filterIsInstance<JavacElement>().map { it.element }.toTypedArray()
+ val fileObject = delegate.createResource(
+ StandardLocation.CLASS_OUTPUT,
+ "",
+ filePath.toString(),
+ *javaOriginatingElements
+ )
+ return fileObject.openOutputStream()
+ }
}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
index 373d9607..7c92ac3 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFiler.kt
@@ -16,8 +16,10 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XFiler
import androidx.room.compiler.processing.XMessager
+import androidx.room.compiler.processing.originatingElementForPoet
import androidx.room.compiler.processing.util.ISSUE_TRACKER_LINK
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
@@ -27,8 +29,11 @@
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.OriginatingElementsHolder
import java.io.OutputStream
+import java.nio.file.Path
import javax.lang.model.element.Element
import javax.tools.Diagnostic
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
internal class KspFiler(
private val delegate: CodeGenerator,
@@ -70,6 +75,27 @@
}
}
+ override fun writeResource(
+ filePath: Path,
+ originatingElements: List<XElement>,
+ mode: XFiler.Mode
+ ): OutputStream {
+ require(filePath.extension != "java" && filePath.extension != "kt") {
+ "Could not create resource file with a source type extension. File must not be " +
+ "neither '.java' nor '.kt', but was: $filePath"
+ }
+ val kspFilerOriginatingElements = originatingElements
+ .mapNotNull { it.originatingElementForPoet() }
+ .toOriginatingElements()
+ return createNewFile(
+ originatingElements = kspFilerOriginatingElements,
+ packageName = filePath.parent?.toString() ?: "",
+ fileName = filePath.nameWithoutExtension,
+ extensionName = filePath.extension,
+ aggregating = mode == XFiler.Mode.Aggregating
+ )
+ }
+
private fun createNewFile(
originatingElements: OriginatingElements,
packageName: String,
@@ -78,14 +104,16 @@
aggregating: Boolean
): OutputStream {
val dependencies = if (originatingElements.isEmpty()) {
- messager.printMessage(
- Diagnostic.Kind.WARNING,
- """
- No dependencies are reported for $fileName which will prevent
- incremental compilation.
- Please file a bug at $ISSUE_TRACKER_LINK.
- """.trimIndent()
- )
+ val isSourceFile = extensionName == "java" || extensionName == "kt"
+ if (isSourceFile) {
+ val filePath = "$packageName.$fileName.$extensionName"
+ messager.printMessage(
+ Diagnostic.Kind.WARNING,
+ "No dependencies reported for generated source $filePath which will" +
+ "prevent incremental compilation.\n" +
+ "Please file a bug at $ISSUE_TRACKER_LINK."
+ )
+ }
Dependencies.ALL_FILES
} else {
Dependencies(
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
new file mode 100644
index 0000000..f69f171
--- /dev/null
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XFilerTest.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import androidx.room.compiler.processing.util.runProcessorTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.io.path.Path
+import org.junit.Assert.fail
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class XFilerTest {
+
+ @Test
+ fun writeResource_sourceFile_java() {
+ runProcessorTest {
+ try {
+ it.processingEnv.filer.writeResource(Path("Test.java"), emptyList()).close()
+ fail("Expected an exception!")
+ } catch (ex: java.lang.IllegalArgumentException) {
+ assertThat(ex.message).isEqualTo(
+ "Could not create resource file with a source type extension. " +
+ "File must not be neither '.java' nor '.kt', but was: Test.java"
+ )
+ }
+ }
+ }
+
+ @Test
+ fun writeResource_sourceFile_kotlin() {
+ runProcessorTest {
+ try {
+ it.processingEnv.filer.writeResource(Path("Test.kt"), emptyList()).close()
+ fail("Expected an exception!")
+ } catch (ex: IllegalArgumentException) {
+ assertThat(ex.message).isEqualTo(
+ "Could not create resource file with a source type extension. " +
+ "File must not be neither '.java' nor '.kt', but was: Test.kt"
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index 4075f36..62c1489 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -32,10 +32,11 @@
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
-import org.junit.Test
import java.io.File
import java.io.OutputStream
import javax.tools.Diagnostic
+import kotlin.io.path.Path
+import org.junit.Test
class KspFilerTest {
@@ -143,6 +144,29 @@
}
}
+ @Test
+ fun writeResource() {
+ runKspTest(
+ sources = emptyList()
+ ) { invocation ->
+ invocation.processingEnv.filer.writeResource(
+ filePath = Path("test.log"),
+ originatingElements = emptyList()
+ ).bufferedWriter(Charsets.UTF_8).use {
+ it.write("Hello!")
+ }
+ invocation.processingEnv.filer.writeResource(
+ filePath = Path("META-INF/services/com.test.Foo"),
+ originatingElements = emptyList()
+ ).bufferedWriter(Charsets.UTF_8).use {
+ it.write("Not a real service...")
+ }
+ invocation.assertCompilationResult {
+ hasNoWarnings()
+ }
+ }
+ }
+
private fun Dependencies?.containsExactlySimpleKotlinClass() {
assertThat(this).isNotNull()
val originatingFiles = this!!.originatingFiles.map { it.fileName }
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 f73b573..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
@@ -149,14 +149,44 @@
val MUTABLE_SET = Set::class.asMutableClassName()
val HASH_SET = XClassName.get("java.util", "HashSet")
val STRING = String::class.asClassName()
- val OPTIONAL = ClassName.get("java.util", "Optional")
+ val OPTIONAL = XClassName.get("java.util", "Optional")
val UUID = XClassName.get("java.util", "UUID")
val BYTE_BUFFER = XClassName.get("java.nio", "ByteBuffer")
val JAVA_CLASS = XClassName.get("java.lang", "Class")
}
-object GuavaBaseTypeNames {
- val OPTIONAL = ClassName.get("com.google.common.base", "Optional")
+object GuavaTypeNames {
+ val OPTIONAL = XClassName.get("com.google.common.base", "Optional")
+ 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",
+ "ImmutableList",
+ "Builder"
+ )
}
object GuavaUtilConcurrentTypeNames {
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 07f549b..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.typeName == 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()
@@ -498,7 +497,7 @@
typeArg = typeArg,
resultAdapter = SingleItemQueryResultAdapter(rowAdapter)
)
- } else if (typeMirror.rawType.typeName == CommonTypeNames.OPTIONAL) {
+ } else if (typeMirror.rawType.asTypeName() == CommonTypeNames.OPTIONAL) {
// Handle java.util.Optional similarly.
val typeArg = typeMirror.typeArguments.first()
// use nullable when finding row adapter as non-null adapters might return
@@ -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 190b633..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
@@ -16,12 +16,10 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
-import androidx.room.ext.GuavaBaseTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
/**
* Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
@@ -36,15 +34,20 @@
cursorVarName: String,
scope: CodeGenScope
) {
- scope.builder().apply {
+ scope.builder.apply {
val valueVarName = scope.getTmpVar("_value")
resultAdapter.convert(valueVarName, cursorVarName, scope)
- addStatement(
- "final $T $L = $T.fromNullable($L)",
- ParameterizedTypeName.get(GuavaBaseTypeNames.OPTIONAL, typeArg.typeName),
- outVarName,
- GuavaBaseTypeNames.OPTIONAL,
- valueVarName
+ addLocalVariable(
+ name = outVarName,
+ typeName = GuavaTypeNames.OPTIONAL.parametrizedBy(
+ typeArg.asTypeName()
+ ),
+ assignExpr = XCodeBlock.of(
+ language = language,
+ format = "%T.fromNullable(%L)",
+ 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 146b474..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
@@ -16,41 +16,53 @@
package androidx.room.solver.query.result
+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.L
-import androidx.room.ext.T
+import androidx.room.ext.GuavaTypeNames
import androidx.room.solver.CodeGenScope
-import com.google.common.collect.ImmutableList
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
class ImmutableListQueryResultAdapter(
private val typeArg: XType,
private val rowAdapter: RowAdapter
) : QueryResultAdapter(listOf(rowAdapter)) {
override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
- scope.builder().apply {
+ scope.builder.apply {
rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
- val collectionType = ParameterizedTypeName
- .get(ClassName.get(ImmutableList::class.java), typeArg.typeName)
- val immutableListBuilderType = ParameterizedTypeName
- .get(ClassName.get(ImmutableList.Builder::class.java), typeArg.typeName)
- val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
- addStatement(
- "final $T $L = $T.<$T>builder()",
- immutableListBuilderType, immutableListBuilderName,
- ClassName.get(ImmutableList::class.java), typeArg.typeName
+ val collectionType = GuavaTypeNames.IMMUTABLE_LIST.parametrizedBy(
+ typeArg.asTypeName()
)
+ val immutableListBuilderType = GuavaTypeNames
+ .IMMUTABLE_LIST_BUILDER.parametrizedBy(typeArg.asTypeName())
+ val immutableListBuilderName = scope.getTmpVar("_immutableListBuilder")
+ addLocalVariable(
+ name = immutableListBuilderName,
+ typeName = immutableListBuilderType,
+ assignExpr = XCodeBlock.ofNewInstance(
+ language = language,
+ GuavaTypeNames.IMMUTABLE_LIST_BUILDER
+ )
+ )
+
val tmpVarName = scope.getTmpVar("_item")
- beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
- addStatement("final $T $L", typeArg.typeName, tmpVarName)
+ beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+ addLocalVariable(
+ name = tmpVarName,
+ typeName = typeArg.asTypeName()
+ )
rowAdapter.convert(tmpVarName, cursorVarName, scope)
- addStatement("$L.add($L)", immutableListBuilderName, tmpVarName)
+ addStatement(
+ "%L.add(%L)",
+ immutableListBuilderName,
+ tmpVarName
+ )
}
endControlFlow()
- addStatement(
- "final $T $L = $L.build()",
- collectionType, outVarName, immutableListBuilderName
+ addLocalVal(
+ name = outVarName,
+ typeName = collectionType,
+ assignExprFormat = "%L.build()",
+ immutableListBuilderName
)
}
}
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/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
index 72001e2..913e44e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/OptionalQueryResultAdapter.kt
@@ -16,12 +16,10 @@
package androidx.room.solver.query.result
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ParameterizedTypeName
/**
* Wraps a row adapter when there is only 1 item in the result, and the result's outer type is
@@ -38,15 +36,20 @@
cursorVarName: String,
scope: CodeGenScope
) {
- scope.builder().apply {
+ scope.builder.apply {
val valueVarName = scope.getTmpVar("_value")
resultAdapter.convert(valueVarName, cursorVarName, scope)
- addStatement(
- "final $T $L = $T.ofNullable($L)",
- ParameterizedTypeName.get(CommonTypeNames.OPTIONAL, typeArg.typeName),
- outVarName,
- CommonTypeNames.OPTIONAL,
- valueVarName
+ addLocalVariable(
+ name = outVarName,
+ typeName = CommonTypeNames.OPTIONAL.parametrizedBy(
+ typeArg.asTypeName()
+ ),
+ assignExpr = XCodeBlock.of(
+ language = language,
+ format = "%T.ofNullable(%L)",
+ CommonTypeNames.OPTIONAL,
+ valueVarName
+ )
)
}
}
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 e89a1d0..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
@@ -1221,6 +1221,93 @@
expectedFilePath = getTestGoldenPath(testName)
)
}
+ @Test
+ fun queryResultAdapter_optional() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM MyEntity")
+ fun queryOfOptional(): java.util.Optional<MyEntity>
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Int,
+ val other: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun queryResultAdapter_guavaOptional() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM MyEntity")
+ fun queryOfOptional(): com.google.common.base.Optional<MyEntity>
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Int,
+ val other: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun queryResultAdapter_immutable_list() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+ import com.google.common.collect.ImmutableList
+
+ @Dao
+ interface MyDao {
+ @Query("SELECT * FROM MyEntity")
+ fun queryOfList(): ImmutableList<MyEntity>
+
+ @Query("SELECT * FROM MyEntity")
+ fun queryOfNullableEntityList(): ImmutableList<MyEntity?>
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Int,
+ val other: String
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
@Test
fun queryResultAdapter_map() {
@@ -1281,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/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt
new file mode 100644
index 0000000..32538d8
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_guavaOptional.kt
@@ -0,0 +1,56 @@
+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.base.Optional
+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 queryOfOptional(): Optional<MyEntity> {
+ val _sql: String = "SELECT * FROM MyEntity"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+ val _value: MyEntity?
+ if (_cursor.moveToFirst()) {
+ val _tmpPk: Int
+ _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ val _tmpOther: String
+ _tmpOther = _cursor.getString(_cursorIndexOfOther)
+ _value = MyEntity(_tmpPk,_tmpOther)
+ } else {
+ _value = null
+ }
+ val _result: Optional<MyEntity> = Optional.fromNullable(_value)
+ 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_immutable_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
new file mode 100644
index 0000000..18f252f
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_immutable_list.kt
@@ -0,0 +1,82 @@
+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.ImmutableList
+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 queryOfList(): ImmutableList<MyEntity> {
+ val _sql: String = "SELECT * FROM MyEntity"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+ val _immutableListBuilder: ImmutableList.Builder<MyEntity> = ImmutableList.Builder()
+ while (_cursor.moveToNext()) {
+ val _item: MyEntity
+ val _tmpPk: Int
+ _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ val _tmpOther: String
+ _tmpOther = _cursor.getString(_cursorIndexOfOther)
+ _item = MyEntity(_tmpPk,_tmpOther)
+ _immutableListBuilder.add(_item)
+ }
+ val _result: ImmutableList<MyEntity> = _immutableListBuilder.build()
+ return _result
+ } finally {
+ _cursor.close()
+ _statement.release()
+ }
+ }
+
+ public override fun queryOfNullableEntityList(): ImmutableList<MyEntity?> {
+ val _sql: String = "SELECT * FROM MyEntity"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+ val _immutableListBuilder: ImmutableList.Builder<MyEntity?> = ImmutableList.Builder()
+ while (_cursor.moveToNext()) {
+ val _item: MyEntity?
+ val _tmpPk: Int
+ _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ val _tmpOther: String
+ _tmpOther = _cursor.getString(_cursorIndexOfOther)
+ _item = MyEntity(_tmpPk,_tmpOther)
+ _immutableListBuilder.add(_item)
+ }
+ val _result: ImmutableList<MyEntity?> = _immutableListBuilder.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/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt
new file mode 100644
index 0000000..a9f9b28
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_optional.kt
@@ -0,0 +1,56 @@
+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 java.lang.Class
+import java.util.Optional
+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 queryOfOptional(): Optional<MyEntity> {
+ val _sql: String = "SELECT * FROM MyEntity"
+ val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+ __db.assertNotSuspendingTransaction()
+ val _cursor: Cursor = query(__db, _statement, false, null)
+ try {
+ val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+ val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+ val _value: MyEntity?
+ if (_cursor.moveToFirst()) {
+ val _tmpPk: Int
+ _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+ val _tmpOther: String
+ _tmpOther = _cursor.getString(_cursorIndexOfOther)
+ _value = MyEntity(_tmpPk,_tmpOther)
+ } else {
+ _value = null
+ }
+ val _result: Optional<MyEntity> = Optional.ofNullable(_value)
+ 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/test/uiautomator/uiautomator/api/current.txt b/test/uiautomator/uiautomator/api/current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/current.txt
+++ b/test/uiautomator/uiautomator/api/current.txt
@@ -117,10 +117,10 @@
ctor public StaleObjectException();
}
- public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
- ctor public UiAutomatorInstrumentationTestRunner();
- method protected android.test.AndroidTestRunner! getAndroidTestRunner();
- method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+ @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+ ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+ method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+ method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
}
@Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
method public void removeWatcher(String?);
method public void resetWatcherTriggers();
method public void runWatchers();
- method public void setCompressedLayoutHeirarchy(boolean);
+ method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+ method public void setCompressedLayoutHierarchy(boolean);
method public void setOrientationLeft() throws android.os.RemoteException;
method public void setOrientationNatural() throws android.os.RemoteException;
method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
+++ b/test/uiautomator/uiautomator/api/public_plus_experimental_current.txt
@@ -117,10 +117,10 @@
ctor public StaleObjectException();
}
- public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
- ctor public UiAutomatorInstrumentationTestRunner();
- method protected android.test.AndroidTestRunner! getAndroidTestRunner();
- method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+ @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+ ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+ method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+ method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
}
@Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
method public void removeWatcher(String?);
method public void resetWatcherTriggers();
method public void runWatchers();
- method public void setCompressedLayoutHeirarchy(boolean);
+ method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+ method public void setCompressedLayoutHierarchy(boolean);
method public void setOrientationLeft() throws android.os.RemoteException;
method public void setOrientationNatural() throws android.os.RemoteException;
method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/api/restricted_current.txt b/test/uiautomator/uiautomator/api/restricted_current.txt
index 93d0ed1..6bf3c87 100644
--- a/test/uiautomator/uiautomator/api/restricted_current.txt
+++ b/test/uiautomator/uiautomator/api/restricted_current.txt
@@ -117,10 +117,10 @@
ctor public StaleObjectException();
}
- public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
- ctor public UiAutomatorInstrumentationTestRunner();
- method protected android.test.AndroidTestRunner! getAndroidTestRunner();
- method protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
+ @Deprecated public class UiAutomatorInstrumentationTestRunner extends android.test.InstrumentationTestRunner {
+ ctor @Deprecated public UiAutomatorInstrumentationTestRunner();
+ method @Deprecated protected android.test.AndroidTestRunner! getAndroidTestRunner();
+ method @Deprecated protected void initializeUiAutomatorTest(androidx.test.uiautomator.UiAutomatorTestCase!);
}
@Deprecated public class UiAutomatorTestCase extends android.test.InstrumentationTestCase {
@@ -187,7 +187,8 @@
method public void removeWatcher(String?);
method public void resetWatcherTriggers();
method public void runWatchers();
- method public void setCompressedLayoutHeirarchy(boolean);
+ method @Deprecated public void setCompressedLayoutHeirarchy(boolean);
+ method public void setCompressedLayoutHierarchy(boolean);
method public void setOrientationLeft() throws android.os.RemoteException;
method public void setOrientationNatural() throws android.os.RemoteException;
method public void setOrientationRight() throws android.os.RemoteException;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
deleted file mode 100644
index b69d9d2..0000000
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2013 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.test.uiautomator;
-
-import android.app.Instrumentation;
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-
-/**
- * A wrapper around {@link Instrumentation} to provide sendStatus function
- *
- * Provided for backwards compatibility purpose. New code should use
- * {@link Instrumentation#sendStatus(int, Bundle)} instead.
- *
- */
-class InstrumentationAutomationSupport implements IAutomationSupport {
-
- private final Instrumentation mInstrumentation;
-
- InstrumentationAutomationSupport(Instrumentation instrumentation) {
- mInstrumentation = instrumentation;
- }
-
- @Override
- public void sendStatus(int resultCode, @NonNull Bundle status) {
- mInstrumentation.sendStatus(resultCode, status);
- }
-}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
index 274e7d9..cc4ea0c 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
@@ -26,7 +26,11 @@
/**
* Test runner for {@link UiAutomatorTestCase}s. Such tests are executed
* on the device and have access to an applications context.
+ * @deprecated as it only handles deprecated {@link UiAutomatorTestCase}s. You should use
+ * {@link UiDevice#getInstance(Instrumentation)} from any test class as long as you have access to
+ * an {@link Instrumentation} instance.
*/
+@Deprecated
public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner {
/**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
index c3e7bf1..bbde508 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
@@ -21,6 +21,8 @@
import android.os.SystemClock;
import android.test.InstrumentationTestCase;
+import androidx.annotation.NonNull;
+
/**
* UI Automator test case that is executed on the device.
* @deprecated It is no longer necessary to extend UiAutomatorTestCase. You can use
@@ -60,7 +62,10 @@
@Deprecated
public IAutomationSupport getAutomationSupport() {
if (mAutomationSupport == null) {
- mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation());
+ Instrumentation instrumentation = getInstrumentation();
+ mAutomationSupport = (int resultCode, @NonNull Bundle status) -> {
+ instrumentation.sendStatus(resultCode, status);
+ };
}
return mAutomationSupport;
}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index d2e6cbf..6dc1982 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -222,8 +222,25 @@
* and searching the hierarchy inefficient are removed.
*
* @param compressed true to enable compression; else, false to disable
+ * @deprecated Typo in function name, should use {@link #setCompressedLayoutHierarchy(boolean)}
+ * instead.
*/
+ @Deprecated
public void setCompressedLayoutHeirarchy(boolean compressed) {
+ this.setCompressedLayoutHierarchy(compressed);
+ }
+
+ /**
+ * Enables or disables layout hierarchy compression.
+ *
+ * If compression is enabled, the layout hierarchy derived from the Accessibility
+ * framework will only contain nodes that are important for uiautomator
+ * testing. Any unnecessary surrounding layout nodes that make viewing
+ * and searching the hierarchy inefficient are removed.
+ *
+ * @param compressed true to enable compression; else, false to disable
+ */
+ public void setCompressedLayoutHierarchy(boolean compressed) {
mCompressed = compressed;
mCachedServiceFlags = -1; // Reset cached accessibility service flags to force an update.
}
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-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
index cd16ae9..0501ac7 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridSlotsReuseTest.kt
@@ -21,13 +21,14 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -319,9 +320,24 @@
fun scrollingBackReusesTheSameSlot() {
lateinit var state: TvLazyGridState
var counter0 = 0
- var counter1 = 10
- var rememberedValue0 = -1
- var rememberedValue1 = -1
+ var counter1 = 0
+
+ val measureCountModifier0 = Modifier.layout { measurable, constraints ->
+ counter0++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
+ val measureCountModifier1 = Modifier.layout { measurable, constraints ->
+ counter1++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
rule.setContent {
state = rememberTvLazyGridState()
TvLazyVerticalGrid(
@@ -330,13 +346,17 @@
state
) {
items(100) {
- if (it == 0) {
- rememberedValue0 = remember { counter0++ }
+ val modifier = when (it) {
+ 0 -> measureCountModifier0
+ 1 -> measureCountModifier1
+ else -> Modifier
}
- if (it == 1) {
- rememberedValue1 = remember { counter1++ }
- }
- Spacer(Modifier.height(itemsSizeDp).fillMaxWidth().testTag("$it"))
+ Spacer(
+ Modifier
+ .height(itemsSizeDp)
+ .testTag("$it")
+ .then(modifier)
+ )
}
}
}
@@ -348,10 +368,10 @@
}
rule.runOnIdle {
- Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
- .that(rememberedValue0).isEqualTo(0)
- Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
- .that(rememberedValue1).isEqualTo(10)
+ Truth.assertWithMessage("Item 0 measured $counter0 times, expected 1.")
+ .that(counter0).isEqualTo(1)
+ Truth.assertWithMessage("Item 1 measured $counter1 times, expected 1.")
+ .that(counter1).isEqualTo(1)
}
rule.onNodeWithTag("0")
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
index 3f108d0..a7bed08 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollAccessibilityTest.kt
@@ -49,6 +49,7 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.IterableSubject
import com.google.common.truth.Truth.assertThat
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -147,6 +148,7 @@
)
}
+ @Ignore // b/242180919
@Test
fun verifyScrollActionsAtEnd() {
createScrollableContent_StartAtEnd()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt
index 3083be7..aea2b2f 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListSlotsReuseTest.kt
@@ -23,13 +23,14 @@
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
@@ -336,9 +337,24 @@
fun scrollingBackReusesTheSameSlot() {
lateinit var state: TvLazyListState
var counter0 = 0
- var counter1 = 10
- var rememberedValue0 = -1
- var rememberedValue1 = -1
+ var counter1 = 0
+
+ val measureCountModifier0 = Modifier.layout { measurable, constraints ->
+ counter0++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
+ val measureCountModifier1 = Modifier.layout { measurable, constraints ->
+ counter1++
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(IntOffset.Zero)
+ }
+ }
+
rule.setContent {
state = rememberTvLazyListState()
TvLazyColumn(
@@ -347,15 +363,18 @@
pivotOffsets = PivotOffsets(parentFraction = 0f)
) {
items(100) {
- if (it == 0) {
- rememberedValue0 = remember { counter0++ }
+ val modifier = when (it) {
+ 0 -> measureCountModifier0
+ 1 -> measureCountModifier1
+ else -> Modifier
}
- if (it == 1) {
- rememberedValue1 = remember { counter1++ }
- }
- Box(
- Modifier.height(itemsSizeDp).fillParentMaxWidth().testTag("$it")
- .focusable())
+ Spacer(
+ Modifier
+ .height(itemsSizeDp)
+ .fillParentMaxWidth()
+ .testTag("$it")
+ .then(modifier)
+ )
}
}
}
@@ -367,10 +386,10 @@
}
rule.runOnIdle {
- Truth.assertWithMessage("Item 0 restored remembered value is $rememberedValue0")
- .that(rememberedValue0).isEqualTo(0)
- Truth.assertWithMessage("Item 1 restored remembered value is $rememberedValue1")
- .that(rememberedValue1).isEqualTo(10)
+ Truth.assertWithMessage("Item 0 measured $counter0 times, expected 1.")
+ .that(counter0).isEqualTo(1)
+ Truth.assertWithMessage("Item 1 measured $counter1 times, expected 1.")
+ .that(counter1).isEqualTo(1)
}
rule.onNodeWithTag("0")
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 9cda913..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
@@ -34,10 +34,11 @@
import androidx.tv.material.TabDefaults
import androidx.tv.material.TabRow
import androidx.tv.material.TabRowDefaults
+import kotlinx.coroutines.delay
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() }),
}
@@ -55,7 +56,12 @@
updateSelectedTab = { selectedTabIndex = it }
)
- LaunchedEffect(selectedTabIndex) { updateSelectedTab(Navigation.values()[selectedTabIndex]) }
+ LaunchedEffect(selectedTabIndex) {
+ // Only update the tab after 250 milliseconds to avoid loading intermediate tabs while
+ // fast scrolling in the TabRow
+ delay(250)
+ updateSelectedTab(Navigation.values()[selectedTabIndex])
+ }
}
/**
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/api/current.ignore b/wear/compose/compose-material/api/current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+ Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -634,7 +634,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index da6559b..a231a61 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -735,7 +735,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
diff --git a/wear/compose/compose-material/api/restricted_current.ignore b/wear/compose/compose-material/api/restricted_current.ignore
new file mode 100644
index 0000000..05e58f9
--- /dev/null
+++ b/wear/compose/compose-material/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+ChangedValue: androidx.wear.compose.material.TimeTextDefaults#TimeFormat12Hours:
+ Field androidx.wear.compose.material.TimeTextDefaults.TimeFormat12Hours has changed value from h:mm a to h:mm
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index c87b5b0..26422ef 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -634,7 +634,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.text.TextStyle timeTextStyle(optional long background, optional long color, optional long fontSize);
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.TimeTextDefaults INSTANCE;
- field public static final String TimeFormat12Hours = "h:mm a";
+ field public static final String TimeFormat12Hours = "h:mm";
field public static final String TimeFormat24Hours = "HH:mm";
}
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/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
index a20b5bc..936958e 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ProgressIndicatorTest.kt
@@ -123,9 +123,9 @@
)
}
rule.waitForIdle()
- // by default fully filled progress approximately takes 23-26% of the control
+ // by default fully filled progress approximately takes 23-27% of the control
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Yellow, 23f..26f)
+ .assertColorInPercentageRange(Color.Yellow, 23f..27f)
rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Red)
}
@@ -142,9 +142,9 @@
}
rule.waitForIdle()
rule.onNodeWithTag(TEST_TAG).captureToImage().assertDoesNotContainColor(Color.Yellow)
- // by default progress track approximately takes 23-26% of the control
+ // by default progress track approximately takes 23-27% of the control
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Red, 23f..26f)
+ .assertColorInPercentageRange(Color.Red, 23f..27f)
}
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@@ -241,7 +241,7 @@
rule.waitForIdle()
// Because of the stroke cap, progress color takes a little bit more space than track color
rule.onNodeWithTag(TEST_TAG).captureToImage()
- .assertColorInPercentageRange(Color.Yellow, 24f..27f)
+ .assertColorInPercentageRange(Color.Yellow, 24f..28f)
rule.onNodeWithTag(TEST_TAG).captureToImage()
.assertColorInPercentageRange(Color.Red, 18f..23f)
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
index ce90129..f3e30ee 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScalingLazyListLayoutInfoTest.kt
@@ -353,53 +353,56 @@
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportOddHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 51, 199)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, false)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemZeroOddHeightViewportEvenHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 50, 199)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, false)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportOddHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 51, 200)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 41, true)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemZeroEvenHeightViewportEvenHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 50, 200)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(0, 40, true)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportOddHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 51, 199)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, false)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemOneOddHeightViewportEvenHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 50, 199)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, false)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportOddHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 51, 200)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 41, true)
}
@Test
fun itemsCorrectScrollPastStartEndAutoCenterItemOneEvenHeightViewportEvenHeightItems() {
- visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 50, 200)
+ visibleItemsAreCorrectAfterScrollingPastEndOfItems(1, 40, true)
}
private fun visibleItemsAreCorrectAfterScrollingPastEndOfItems(
autoCenterItem: Int,
localItemSizePx: Int,
- viewportSizePx: Int
+ viewPortSizeEven: Boolean
) {
lateinit var state: ScalingLazyListState
lateinit var scope: CoroutineScope
rule.setContent {
with(LocalDensity.current) {
+ val viewportSizePx =
+ (((localItemSizePx * 4 + defaultItemSpacingPx * 3) / 2) * 2) +
+ if (viewPortSizeEven) 0 else 1
scope = rememberCoroutineScope()
ScalingLazyColumn(
state = rememberScalingLazyListState(
@@ -504,7 +507,7 @@
ScalingLazyColumn(
state = rememberScalingLazyListState(centerItemIndex).also { state = it },
modifier = Modifier.requiredSize(
- itemSizeDp * 4
+ itemSizeDp * 4 + defaultItemSpacingDp * 3
),
) {
items(6) {
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
index c81b71a..86a4f4b 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
@@ -35,7 +35,6 @@
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import org.junit.Rule
import org.junit.Test
@@ -47,7 +46,6 @@
@get:Rule
val rule = createComposeRule()
- @FlakyTest(bugId = 259134313)
@Test
fun hidesTimeTextWithScalingLazyColumn() {
lateinit var scrollState: ScalingLazyListState
@@ -189,7 +187,6 @@
item {
ListHeader { Text("Chips") }
}
-
items(5) { i ->
ChipTest(Modifier.fillParentMaxHeight(0.5f), i)
}
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
index 382300c..e9644a6 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/TimeTextTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.unit.sp
import androidx.wear.compose.foundation.curvedComposable
import java.util.Calendar
+import java.util.TimeZone
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -541,6 +542,22 @@
}
assertEquals(convertedTime, actualTime)
}
+
+ @Test
+ fun formats_current_time_12H() {
+ val currentTimeInMillis = 1631544258000L // 2021-09-13 14:44:18
+ val expectedTime = "2:44"
+ TimeZone.setDefault(TimeZone.getTimeZone("UTC"))
+
+ var actualTime: String? = null
+ rule.setContentWithTheme {
+ actualTime = currentTime(
+ { currentTimeInMillis },
+ TimeTextDefaults.TimeFormat12Hours
+ ).value
+ }
+ assertEquals(expectedTime, actualTime)
+ }
}
private const val LINEAR_ITEM_TAG = "LINEAR_ITEM_TAG"
diff --git a/wear/compose/compose-material/src/androidMain/baseline-prof.txt b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
index 2eb04f1..f2a2297 100644
--- a/wear/compose/compose-material/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-material/src/androidMain/baseline-prof.txt
@@ -1,5 +1,5 @@
HPLandroidx/wear/compose/material/AbstractPlaceholderModifier;->**(**)**
-PLandroidx/wear/compose/material/AnimationKt**->**(**)**
+SPLandroidx/wear/compose/material/AnimationKt**->**(**)**
PLandroidx/wear/compose/material/AutoCenteringParams;->**(**)**
Landroidx/wear/compose/material/ButtonBorder;
Landroidx/wear/compose/material/ButtonColors;
@@ -8,7 +8,7 @@
HSPLandroidx/wear/compose/material/CardDefaults;->**(**)**
HSPLandroidx/wear/compose/material/CardKt**->**(**)**
Landroidx/wear/compose/material/CheckboxColors;
-HPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/CheckboxDefaults;->**(**)**
Landroidx/wear/compose/material/ChipBorder;
Landroidx/wear/compose/material/ChipColors;
HSPLandroidx/wear/compose/material/ChipDefaults;->**(**)**
@@ -22,16 +22,16 @@
HSPLandroidx/wear/compose/material/CurvedTextKt**->**(**)**
HSPLandroidx/wear/compose/material/DefaultButtonBorder;->**(**)**
HSPLandroidx/wear/compose/material/DefaultButtonColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultCheckboxColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultChipBorder;->**(**)**
HSPLandroidx/wear/compose/material/DefaultChipColors;->**(**)**
HSPLandroidx/wear/compose/material/DefaultInlineSliderColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultRadioButtonColors;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingLazyListItemInfo;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingLazyListLayoutInfo;->**(**)**
HSPLandroidx/wear/compose/material/DefaultScalingParams;->**(**)**
HSPLandroidx/wear/compose/material/DefaultSplitToggleChipColors;->**(**)**
-HPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
+HSPLandroidx/wear/compose/material/DefaultSwitchColors;->**(**)**
SPLandroidx/wear/compose/material/DefaultTimeSource;->**(**)**
HSPLandroidx/wear/compose/material/DefaultTimeSourceKt**->**(**)**
SPLandroidx/wear/compose/material/DefaultToggleButtonColors;->**(**)**
@@ -53,17 +53,17 @@
HSPLandroidx/wear/compose/material/MaterialTheme;->**(**)**
SPLandroidx/wear/compose/material/MaterialThemeKt**->**(**)**
HSPLandroidx/wear/compose/material/Modifiers;->**(**)**
-PLandroidx/wear/compose/material/PainterWithBrushOverlay;->**(**)**
HSPLandroidx/wear/compose/material/PickerDefaults;->**(**)**
HSPLandroidx/wear/compose/material/PickerKt**->**(**)**
SPLandroidx/wear/compose/material/PickerScopeImpl;->**(**)**
HSPLandroidx/wear/compose/material/PickerState;->**(**)**
+PLandroidx/wear/compose/material/PlaceholderBackgroundPainter;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderDefaults;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderKt**->**(**)**
PLandroidx/wear/compose/material/PlaceholderModifier;->**(**)**
HPLandroidx/wear/compose/material/PlaceholderShimmerModifier;->**(**)**
PLandroidx/wear/compose/material/PlaceholderStage;->**(**)**
-PLandroidx/wear/compose/material/PlaceholderState;->**(**)**
+HPLandroidx/wear/compose/material/PlaceholderState;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorAlignment;->**(**)**
HSPLandroidx/wear/compose/material/PositionIndicatorKt**->**(**)**
Landroidx/wear/compose/material/PositionIndicatorState;
@@ -71,8 +71,9 @@
PLandroidx/wear/compose/material/ProgressIndicatorDefaults;->**(**)**
HPLandroidx/wear/compose/material/ProgressIndicatorKt**->**(**)**
Landroidx/wear/compose/material/R;
-HPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
-HSPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
+Landroidx/wear/compose/material/RadioButtonColors;
+HSPLandroidx/wear/compose/material/RadioButtonDefaults;->**(**)**
+SPLandroidx/wear/compose/material/RangeDefaults;->**(**)**
SPLandroidx/wear/compose/material/RangeDefaultsKt**->**(**)**
SPLandroidx/wear/compose/material/RangeIcons;->**(**)**
HSPLandroidx/wear/compose/material/ResistanceConfig;->**(**)**
@@ -100,7 +101,6 @@
HSPLandroidx/wear/compose/material/ShapesKt**->**(**)**
HSPLandroidx/wear/compose/material/SliderKt**->**(**)**
Landroidx/wear/compose/material/SplitToggleChipColors;
-SPLandroidx/wear/compose/material/SqueezeMotion;->**(**)**
SPLandroidx/wear/compose/material/StepperDefaults;->**(**)**
HSPLandroidx/wear/compose/material/StepperKt**->**(**)**
Landroidx/wear/compose/material/SwipeProgress;
@@ -113,7 +113,7 @@
HSPLandroidx/wear/compose/material/SwipeableKt**->**(**)**
HSPLandroidx/wear/compose/material/SwipeableState;->**(**)**
Landroidx/wear/compose/material/SwitchColors;
-HPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
+HSPLandroidx/wear/compose/material/SwitchDefaults;->**(**)**
HSPLandroidx/wear/compose/material/TextKt**->**(**)**
Landroidx/wear/compose/material/ThresholdConfig;
SPLandroidx/wear/compose/material/TimeBroadcastReceiver;->**(**)**
@@ -126,8 +126,8 @@
Landroidx/wear/compose/material/ToggleChipColors;
HSPLandroidx/wear/compose/material/ToggleChipDefaults;->**(**)**
HSPLandroidx/wear/compose/material/ToggleChipKt**->**(**)**
-HPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
-PLandroidx/wear/compose/material/ToggleStage;->**(**)**
+HSPLandroidx/wear/compose/material/ToggleControlKt**->**(**)**
+SPLandroidx/wear/compose/material/ToggleStage;->**(**)**
HSPLandroidx/wear/compose/material/Typography;->**(**)**
HSPLandroidx/wear/compose/material/TypographyKt**->**(**)**
HSPLandroidx/wear/compose/material/VignetteKt**->**(**)**
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
index 767a746..5cdfc9c 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -40,7 +40,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
+import androidx.compose.runtime.withFrameNanos
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
@@ -56,7 +56,6 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.offset
-import kotlinx.coroutines.flow.first
/**
* Receiver scope which is used by [ScalingLazyColumn].
@@ -393,7 +392,13 @@
.clipToBounds()
.verticalNegativePadding(extraPadding)
.onGloballyPositioned {
- initialized = true
+ val layoutInfo = state.layoutInfo
+ if (!initialized &&
+ layoutInfo is DefaultScalingLazyListLayoutInfo &&
+ layoutInfo.readyForInitialScroll
+ ) {
+ initialized = true
+ }
},
horizontalAlignment = horizontalAlignment,
contentPadding = combinedPaddingValues,
@@ -428,12 +433,10 @@
}
if (initialized) {
LaunchedEffect(state) {
- snapshotFlow {
- (state.layoutInfo as DefaultScalingLazyListLayoutInfo)
- .readyForInitialScroll
- }.first {
- it
- }
+ // TODO(b/254115946) LaunchedEffect is run synchronously under testing,
+ // which causes compose runtime issues. Work is under way to fix this -
+ // the workaround for now is to call withFrameNanos{}
+ withFrameNanos {}
state.scrollToInitialItem()
}
}
@@ -732,4 +735,4 @@
layout(placeable.measuredWidth, constraints.maxHeight) {
placeable.place(0, -extraPadding.roundToPx())
}
-}
\ No newline at end of file
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index 3574a40..f2150c7 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -22,7 +22,6 @@
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
@@ -45,7 +44,12 @@
public fun Modifier.scrollAway(
scrollState: ScrollState,
offset: Dp = 0.dp,
-): Modifier = scrollAway { scrollState.value - offset.toPx() }
+): Modifier = scrollAway {
+ ScrollParams(
+ valid = true,
+ yPx = scrollState.value - offset.toPx()
+ )
+}
/**
* Scroll an item vertically in/out of view based on a [LazyListState].
@@ -62,10 +66,13 @@
itemIndex: Int = 0,
offset: Dp = 0.dp,
): Modifier =
- scrollAway(itemIndex < scrollState.layoutInfo.totalItemsCount) {
- scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
- -it.offset - offset.toPx()
- }
+ scrollAway {
+ ScrollParams(
+ valid = itemIndex < scrollState.layoutInfo.totalItemsCount,
+ yPx = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+ -it.offset - offset.toPx()
+ }
+ )
}
/**
@@ -83,13 +90,16 @@
itemIndex: Int = 1,
offset: Dp = 0.dp,
): Modifier =
- scrollAway(itemIndex < scrollState.layoutInfo.totalItemsCount) {
- scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
- -it.offset - offset.toPx()
- }
+ scrollAway {
+ ScrollParams(
+ valid = itemIndex < scrollState.layoutInfo.totalItemsCount,
+ yPx = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+ -it.offset - offset.toPx()
+ }
+ )
}
-private fun Modifier.scrollAway(valid: Boolean = true, yPxFn: Density.() -> Float?): Modifier =
+private fun Modifier.scrollAway(scrollFn: Density.() -> ScrollParams): Modifier =
this.then(
object : LayoutModifier {
override fun MeasureScope.measure(
@@ -97,40 +107,41 @@
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
- val yPx = yPxFn()
- if (!valid) {
- // For invalid inputs, don't scroll the content away - just show it.
- return layout(placeable.width, placeable.height) {
- placeable.placeRelative(0, 0)
- }
- } else if (yPx == null) {
- // For valid inputs, but no y offset provided, hide the content.
- return object : MeasureResult {
- override val width = 0
- override val height = 0
- override val alignmentLines = mapOf<AlignmentLine, Int>()
- override fun placeChildren() {}
- }
- } else {
- // Valid input and a y offset is provided - apply fade, scale and offset.
- return layout(placeable.width, placeable.height) {
- val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
- val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
- val offsetY = -(maxOffset.toPx() * progress).toInt()
+ return layout(placeable.width, placeable.height) {
+ placeable.placeWithLayer(0, 0) {
+ val scrollParams = scrollFn()
+ val (motionFraction: Float, offsetY) =
+ if (!scrollParams.valid) {
+ // When the itemIndex is invalid, just show the content anyway.
+ 1f to 0f
+ } else if (scrollParams.yPx == null) {
+ // When itemIndex is valid but yPx is null, we infer that
+ // the item is not in the visible items list, so hide it.
+ 0f to 0f
+ } else {
+ // Scale, fade and scroll the content to scroll it away.
+ val progress: Float =
+ (scrollParams.yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
+ val motionFraction: Float =
+ lerp(minMotionOut, maxMotionOut, progress)
+ val offsetY = -(maxOffset.toPx() * progress)
+ motionFraction to offsetY
+ }
- placeable.placeWithLayer(0, offsetY) {
- alpha = motionFraction
- scaleX = motionFraction
- scaleY = motionFraction
- transformOrigin =
- TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
- }
+ alpha = motionFraction
+ scaleX = motionFraction
+ scaleY = motionFraction
+ translationY = offsetY
+ transformOrigin =
+ TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
}
}
}
}
)
+private data class ScrollParams(val valid: Boolean, val yPx: Float?)
+
// The scroll motion effects take place between 0dp and 36dp.
internal val maxScrollOut = 36.dp
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
index 2ff5ef9..cb8a7d4 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/TimeText.kt
@@ -157,7 +157,7 @@
/**
* Default format for 12h clock.
*/
- const val TimeFormat12Hours = "h:mm a"
+ const val TimeFormat12Hours = "h:mm"
/**
* The default content padding used by [TimeText]
diff --git a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
index e343b99..5068468 100644
--- a/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
+++ b/wear/compose/compose-navigation/src/androidMain/baseline-prof.txt
@@ -1,4 +1,3 @@
-Landroidx/wear/compose/navigation/ComposableSingletons;
HSPLandroidx/wear/compose/navigation/NavGraphBuilderKt**->**(**)**
SPLandroidx/wear/compose/navigation/SwipeDismissableNavHostControllerKt**->**(**)**
HSPLandroidx/wear/compose/navigation/SwipeDismissableNavHostKt**->**(**)**
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 9744cae..6afd2a2 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -61,8 +61,8 @@
@Composable
fun ScrollAwayLazyColumnDemo2() {
LazyColumnCardDemo(
- offset = -195.dp,
- itemIndex = 2,
+ offset = -100.dp,
+ itemIndex = 1,
initialVisibleItemIndex = 2
)
}
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-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index dc02bd9..85af687 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -39,7 +39,6 @@
import androidx.test.screenshot.AndroidXScreenshotTestRule
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotBoundsType
import androidx.wear.watchface.ContentDescriptionLabel
import androidx.wear.watchface.DrawMode
@@ -242,30 +241,6 @@
return value!!
}
- /**
- * Updates the complications for [interactiveInstance] and waits until they have been applied.
- */
- protected fun updateComplicationsBlocking(
- interactiveInstance: InteractiveWatchFaceClient,
- slotIdToComplicationData: Map<Int, ComplicationData>
- ) {
- val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
- var slot: ComplicationSlot
-
- runBlocking {
- slot = engine.deferredWatchFaceImpl.await()
- .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
- }
-
- val updateCountDownLatch = CountDownLatch(1)
- handlerCoroutineScope.launch {
- slot.complicationData.collect { updateCountDownLatch.countDown() }
- }
-
- interactiveInstance.updateComplicationData(slotIdToComplicationData)
- assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
- }
-
protected fun tapOnComplication(
interactiveInstance: InteractiveWatchFaceClient,
slotId: Int
@@ -471,8 +446,7 @@
fun updateComplicationData() {
val interactiveInstance = getOrCreateTestSubject()
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
rangedValueComplicationBuilder().build(),
@@ -1132,8 +1106,7 @@
surfaceHolder
)
val interactiveInstance = getOrCreateTestSubject(wallpaperService)
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
rangedValueComplicationBuilder()
@@ -1194,8 +1167,7 @@
)
)
- updateComplicationsBlocking(
- interactiveInstance,
+ interactiveInstance.updateComplicationData(
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
timelineComplication.toApiComplicationData()
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
index 14afc68..4dccf73 100644
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataTimelineTest.java
@@ -131,24 +131,27 @@
public void timeEntryToString() {
assertThat(TIMELINE_A.toString()).isEqualTo(
"ComplicationDataTimeline(defaultComplicationData=ShortTextComplicationData("
- + "text=ComplicationText{mSurroundingText=Hello, mTimeDependentText=null}, "
- + "title=null, monochromaticImage=null, smallImage=null, "
+ + "text=ComplicationText{mSurroundingText=Hello, mTimeDependentText=null, "
+ + "mStringExpression=null}, title=null, monochromaticImage=null, "
+ + "smallImage=null, contentDescription=ComplicationText{"
+ + "mSurroundingText=, mTimeDependentText=null, mStringExpression=null}, "
+ + "tapActionLostDueToSerialization=false, tapAction=null, "
+ + "validTimeRange=TimeRange(startDateTimeMillis="
+ + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
+ + "+1000000000-12-31T23:59:59.999999999Z), dataSource=null, "
+ + "persistencePolicy=0, displayPolicy=0), timelineEntries=["
+ + "TimelineEntry(validity=TimeInterval(start=1970-01-02T03:46:40Z, "
+ + "end=1970-01-03T07:33:20Z), complicationData=ShortTextComplicationData("
+ + "text=ComplicationText{mSurroundingText=Updated, "
+ + "mTimeDependentText=null, mStringExpression=null}, title=null, "
+ + "monochromaticImage=null, smallImage=null, "
+ "contentDescription=ComplicationText{mSurroundingText=, "
- + "mTimeDependentText=null}, tapActionLostDueToSerialization=false, "
- + "tapAction=null, validTimeRange=TimeRange("
- + "startDateTimeMillis=-1000000000-01-01T00:00:00Z, "
- + "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), "
- + "dataSource=null, persistencePolicy=0, displayPolicy=0), "
- + "timelineEntries=[TimelineEntry(validity"
- + "=TimeInterval(start=1970-01-02T03:46:40Z, end=1970-01-03T07:33:20Z), "
- + "complicationData=ShortTextComplicationData(text=ComplicationText{"
- + "mSurroundingText=Updated, mTimeDependentText=null}, title=null, "
- + "monochromaticImage=null, smallImage=null, contentDescription="
- + "ComplicationText{mSurroundingText=, mTimeDependentText=null}, "
- + "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange="
- + "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, "
- + "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), "
- + "dataSource=null, persistencePolicy=0, displayPolicy=0))])"
+ + "mTimeDependentText=null, mStringExpression=null}, "
+ + "tapActionLostDueToSerialization=false, tapAction=null, "
+ + "validTimeRange=TimeRange(startDateTimeMillis="
+ + "-1000000000-01-01T00:00:00Z, endDateTimeMillis="
+ + "+1000000000-12-31T23:59:59.999999999Z), dataSource=null, "
+ + "persistencePolicy=0, displayPolicy=0))])"
);
}
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index c473a58..8d23b7f 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -86,14 +86,14 @@
public final class DataKt {
}
- public final class DynamicFloatKt {
- }
-
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
+ public final class FloatExpressionKt {
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index fdbd262..9078277 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -89,23 +89,17 @@
public final class DataKt {
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public abstract class DynamicFloat {
- ctor public DynamicFloat();
- method public abstract byte[] asByteArray();
- }
-
- public final class DynamicFloatKt {
- }
-
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
+ public final class FloatExpressionKt {
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
method public float getTargetValue();
@@ -114,7 +108,6 @@
method public float getValue();
property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
- property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
property public final float targetValue;
@@ -127,7 +120,6 @@
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class GoalProgressComplicationData.Builder {
ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public GoalProgressComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
@@ -276,7 +268,6 @@
public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
method public float getMax();
method public float getMin();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -287,7 +278,6 @@
method public int getValueType();
property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
- property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
property public final float max;
property public final float min;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
@@ -305,7 +295,6 @@
public static final class RangedValueComplicationData.Builder {
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public RangedValueComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index 83c88a5..c3d723ff 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -86,14 +86,14 @@
public final class DataKt {
}
- public final class DynamicFloatKt {
- }
-
public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
ctor public EmptyComplicationData();
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
+ public final class FloatExpressionKt {
+ }
+
@RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 19f21c4..23a2fc0 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -37,8 +37,8 @@
import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
import androidx.wear.watchface.complications.data.ComplicationPersistencePolicy
-import androidx.wear.watchface.complications.data.DynamicFloat
-import androidx.wear.watchface.complications.data.toDynamicFloat
+import androidx.wear.watchface.complications.data.FloatExpression
+import androidx.wear.watchface.complications.data.toFloatExpression
import java.io.IOException
import java.io.InvalidObjectException
import java.io.ObjectInputStream
@@ -177,8 +177,8 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
oos.writeFloat(complicationData.rangedValue)
}
- if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
- oos.writeNullable(complicationData.rangedDynamicValue) {
+ if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
+ oos.writeNullable(complicationData.rangedValueExpression) {
oos.writeByteArray(it.asByteArray())
}
}
@@ -317,9 +317,9 @@
if (isFieldValidForType(FIELD_VALUE, type)) {
fields.putFloat(FIELD_VALUE, ois.readFloat())
}
- if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+ if (isFieldValidForType(FIELD_VALUE_EXPRESSION, type)) {
ois.readNullable { ois.readByteArray() }?.let {
- fields.putByteArray(FIELD_DYNAMIC_VALUE, it)
+ fields.putByteArray(FIELD_VALUE_EXPRESSION, it)
}
}
if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
@@ -594,21 +594,21 @@
}
/**
- * Returns true if the ComplicationData contains a ranged dynamic value. I.e. if
- * [rangedDynamicValue] can succeed.
+ * Returns true if the ComplicationData contains a ranged value expression. I.e. if
+ * [rangedValueExpression] can succeed.
*/
- fun hasRangedDynamicValue(): Boolean = isFieldValidForType(FIELD_DYNAMIC_VALUE, type)
+ fun hasRangedValueExpression(): Boolean = isFieldValidForType(FIELD_VALUE_EXPRESSION, type)
/**
- * Returns the *dynamicValue* field for this complication.
+ * Returns the *valueExpression* field for this complication.
*
* Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
* [TYPE_GOAL_PROGRESS].
*/
- val rangedDynamicValue: DynamicFloat?
+ val rangedValueExpression: FloatExpression?
get() {
- checkFieldValidForTypeWithoutThrowingException(FIELD_DYNAMIC_VALUE, type)
- return fields.getByteArray(FIELD_DYNAMIC_VALUE)?.toDynamicFloat()
+ checkFieldValidForTypeWithoutThrowingException(FIELD_VALUE_EXPRESSION, type)
+ return fields.getByteArray(FIELD_VALUE_EXPRESSION)?.toFloatExpression()
}
/**
@@ -1264,15 +1264,15 @@
fun setRangedValue(value: Float?) = apply { putOrRemoveField(FIELD_VALUE, value) }
/**
- * Sets the *dynamicValue* field. It is evaluated to a value with the same limitations as
+ * Sets the *valueExpression* field. It is evaluated to a value with the same limitations as
* [setRangedValue].
*
* Returns this Builder to allow chaining.
*
* @throws IllegalStateException if this field is not valid for the complication type
*/
- fun setRangedDynamicValue(value: DynamicFloat?) =
- apply { putOrRemoveField(FIELD_DYNAMIC_VALUE, value?.asByteArray()) }
+ fun setRangedValueExpression(value: FloatExpression?) =
+ apply { putOrRemoveField(FIELD_VALUE_EXPRESSION, value?.asByteArray()) }
/**
* Sets the *value type* field which provides meta data about the value. This is
@@ -1923,7 +1923,7 @@
private const val FIELD_TIMELINE_ENTRIES = "TIMELINE"
private const val FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE"
private const val FIELD_VALUE = "VALUE"
- private const val FIELD_DYNAMIC_VALUE = "DYNAMIC_VALUE"
+ private const val FIELD_VALUE_EXPRESSION = "VALUE_EXPRESSION"
private const val FIELD_VALUE_TYPE = "VALUE_TYPE"
// Experimental fields, these are subject to change without notice.
@@ -1991,7 +1991,7 @@
TYPE_EMPTY to setOf(),
TYPE_SHORT_TEXT to setOf(),
TYPE_LONG_TEXT to setOf(),
- TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+ TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_VALUE_EXPRESSION)),
TYPE_ICON to setOf(),
TYPE_SMALL_IMAGE to setOf(),
TYPE_LARGE_IMAGE to setOf(),
@@ -1999,7 +1999,7 @@
TYPE_NO_DATA to setOf(),
EXP_TYPE_PROTO_LAYOUT to setOf(),
EXP_TYPE_LIST to setOf(),
- TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+ TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_VALUE_EXPRESSION)),
TYPE_WEIGHTED_ELEMENTS to setOf(),
)
@@ -2105,7 +2105,7 @@
FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
FIELD_TAP_ACTION,
FIELD_VALUE,
- FIELD_DYNAMIC_VALUE,
+ FIELD_VALUE_EXPRESSION,
FIELD_VALUE_TYPE,
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
index 16f9352..171680f 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationText.java
@@ -37,6 +37,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.wear.watchface.complications.data.StringExpression;
import java.io.IOException;
import java.io.InvalidObjectException;
@@ -75,21 +76,25 @@
return false;
}
ComplicationText that = (ComplicationText) o;
+ if (!Objects.equals(mStringExpression, that.mStringExpression)) {
+ return false;
+ }
if (!Objects.equals(mTimeDependentText, that.mTimeDependentText)) {
return false;
}
if (mSurroundingText == null) {
+ return that.mSurroundingText == null;
+ } else {
if (that.mSurroundingText != null) {
- return false;
+ return mSurroundingText.toString().contentEquals(that.mSurroundingText);
}
- return true;
}
- return mSurroundingText.toString().contentEquals(that.mSurroundingText);
+ return true;
}
@Override
public int hashCode() {
- return Objects.hash(mSurroundingText, mTimeDependentText);
+ return Objects.hash(mSurroundingText, mTimeDependentText, mStringExpression);
}
@NonNull
@@ -97,7 +102,8 @@
public String toString() {
return "ComplicationText{" + "mSurroundingText="
+ ComplicationData.maybeRedact(mSurroundingText)
- + ", mTimeDependentText=" + mTimeDependentText + '}';
+ + ", mTimeDependentText=" + mTimeDependentText + ", mStringExpression="
+ + mStringExpression + "}";
}
/** @hide */
@@ -229,6 +235,7 @@
private static final String KEY_DIFFERENCE_STYLE = "difference_style";
private static final String KEY_DIFFERENCE_SHOW_NOW_TEXT = "show_now_text";
private static final String KEY_DIFFERENCE_MINIMUM_UNIT = "minimum_unit";
+ private static final String KEY_DYNAMIC_STRING = "dynamic_string";
private static final String KEY_FORMAT_FORMAT_STRING = "format_format_string";
private static final String KEY_FORMAT_STYLE = "format_style";
private static final String KEY_FORMAT_TIME_ZONE = "format_time_zone";
@@ -264,8 +271,13 @@
* must be not null and {@link #getTextAt} will return just the time-dependent value relative to
* the given time.
*/
+ @Nullable
private final TimeDependentText mTimeDependentText;
+ /** A [StringExpression] which will be evaluated by the system on the WatchFace's behalf. */
+ @Nullable
+ private final StringExpression mStringExpression;
+
/** Used to replace occurrences of ^1 with time dependent text and ignore ^[2-9]. */
private final CharSequence[] mTemplateValues =
new CharSequence[]{"", "^2", "^3", "^4", "^5", "^6", "^7", "^8", "^9"};
@@ -277,16 +289,28 @@
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
public ComplicationText(@Nullable CharSequence surroundingText,
- @Nullable TimeDependentText timeDependentText) {
+ @Nullable TimeDependentText timeDependentText,
+ @Nullable StringExpression stringExpression) {
mSurroundingText = surroundingText;
mTimeDependentText = timeDependentText;
+ mStringExpression = stringExpression;
checkFields();
}
+ public ComplicationText(@NonNull StringExpression stringExpression) {
+ this(/* surroundingText = */ null, /* timeDependentText = */ null, stringExpression);
+ }
+
private ComplicationText(@NonNull Parcel in) {
Bundle bundle = in.readBundle(getClass().getClassLoader());
mSurroundingText = bundle.getCharSequence(KEY_SURROUNDING_STRING);
+ if (bundle.containsKey(KEY_DYNAMIC_STRING)) {
+ mStringExpression = new StringExpression(bundle.getByteArray(KEY_DYNAMIC_STRING));
+ } else {
+ mStringExpression = null;
+ }
+
if (bundle.containsKey(KEY_DIFFERENCE_STYLE)
&& bundle.containsKey(KEY_DIFFERENCE_PERIOD_START)
&& bundle.containsKey(KEY_DIFFERENCE_PERIOD_END)) {
@@ -317,30 +341,48 @@
private static class SerializedForm implements Serializable {
CharSequence mSurroundingText;
TimeDependentText mTimeDependentText;
+ StringExpression mStringExpression;
SerializedForm(@Nullable CharSequence surroundingText,
- @Nullable TimeDependentText timeDependentText) {
+ @Nullable TimeDependentText timeDependentText,
+ @Nullable StringExpression stringExpression) {
mSurroundingText = surroundingText;
mTimeDependentText = timeDependentText;
+ mStringExpression = stringExpression;
}
private void writeObject(ObjectOutputStream oos) throws IOException {
CharSequenceSerializableHelper.writeToStream(mSurroundingText, oos);
oos.writeObject(mTimeDependentText);
+ if (mStringExpression == null) {
+ oos.writeInt(0);
+ } else {
+ byte[] bytes = mStringExpression.asByteArray();
+ oos.writeInt(bytes.length);
+ oos.write(bytes);
+ }
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
mSurroundingText = CharSequenceSerializableHelper.readFromStream(ois);
mTimeDependentText = (TimeDependentText) ois.readObject();
+ int length = ois.readInt();
+ if (length == 0) {
+ mStringExpression = null;
+ } else {
+ byte[] bytes = new byte[length];
+ ois.readFully(bytes);
+ mStringExpression = new StringExpression(bytes);
+ }
}
Object readResolve() {
- return new ComplicationText(mSurroundingText, mTimeDependentText);
+ return new ComplicationText(mSurroundingText, mTimeDependentText, mStringExpression);
}
}
Object writeReplace() {
- return new SerializedForm(mSurroundingText, mTimeDependentText);
+ return new SerializedForm(mSurroundingText, mTimeDependentText, mStringExpression);
}
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
@@ -364,9 +406,10 @@
}
private void checkFields() {
- if (mSurroundingText == null && mTimeDependentText == null) {
+ if (mSurroundingText == null && mTimeDependentText == null && mStringExpression == null) {
throw new IllegalStateException(
- "One of mSurroundingText and mTimeDependentText must be non-null");
+ "One of mSurroundingText, mTimeDependentText and mStringExpression must be"
+ + " non-null");
}
}
@@ -381,22 +424,29 @@
Bundle bundle = new Bundle();
bundle.putCharSequence(KEY_SURROUNDING_STRING, mSurroundingText);
- if (mTimeDependentText instanceof TimeDifferenceText) {
- TimeDifferenceText timeDiffText = (TimeDifferenceText) mTimeDependentText;
- bundle.putLong(KEY_DIFFERENCE_PERIOD_START, timeDiffText.getReferencePeriodStart());
- bundle.putLong(KEY_DIFFERENCE_PERIOD_END, timeDiffText.getReferencePeriodEnd());
- bundle.putInt(KEY_DIFFERENCE_STYLE, timeDiffText.getStyle());
- bundle.putBoolean(KEY_DIFFERENCE_SHOW_NOW_TEXT, timeDiffText.shouldShowNowText());
- if (timeDiffText.getMinimumUnit() != null) {
- bundle.putString(KEY_DIFFERENCE_MINIMUM_UNIT, timeDiffText.getMinimumUnit().name());
- }
- } else if (mTimeDependentText instanceof TimeFormatText) {
- TimeFormatText timeFormatText = (TimeFormatText) mTimeDependentText;
- bundle.putString(KEY_FORMAT_FORMAT_STRING, timeFormatText.getFormatString());
- bundle.putInt(KEY_FORMAT_STYLE, timeFormatText.getStyle());
- TimeZone timeZone = timeFormatText.getTimeZone();
- if (timeZone != null) {
- bundle.putString(KEY_FORMAT_TIME_ZONE, timeZone.getID());
+ if (mStringExpression != null) {
+ bundle.putByteArray(KEY_DYNAMIC_STRING, mStringExpression.asByteArray());
+ }
+
+ if (mTimeDependentText != null) {
+ if (mTimeDependentText instanceof TimeDifferenceText) {
+ TimeDifferenceText timeDiffText = (TimeDifferenceText) mTimeDependentText;
+ bundle.putLong(KEY_DIFFERENCE_PERIOD_START, timeDiffText.getReferencePeriodStart());
+ bundle.putLong(KEY_DIFFERENCE_PERIOD_END, timeDiffText.getReferencePeriodEnd());
+ bundle.putInt(KEY_DIFFERENCE_STYLE, timeDiffText.getStyle());
+ bundle.putBoolean(KEY_DIFFERENCE_SHOW_NOW_TEXT, timeDiffText.shouldShowNowText());
+ if (timeDiffText.getMinimumUnit() != null) {
+ bundle.putString(KEY_DIFFERENCE_MINIMUM_UNIT,
+ timeDiffText.getMinimumUnit().name());
+ }
+ } else if (mTimeDependentText instanceof TimeFormatText) {
+ TimeFormatText timeFormatText = (TimeFormatText) mTimeDependentText;
+ bundle.putString(KEY_FORMAT_FORMAT_STRING, timeFormatText.getFormatString());
+ bundle.putInt(KEY_FORMAT_STYLE, timeFormatText.getStyle());
+ TimeZone timeZone = timeFormatText.getTimeZone();
+ if (timeZone != null) {
+ bundle.putString(KEY_FORMAT_TIME_ZONE, timeZone.getID());
+ }
}
}
@@ -411,6 +461,11 @@
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY)
public TimeDependentText getTimeDependentText() {
+ if (mStringExpression != null) {
+ throw new UnsupportedOperationException("getTimeDependentText not supported for "
+ + "StringExpressions");
+ }
+ assert mTimeDependentText != null;
return mTimeDependentText;
}
@@ -430,6 +485,11 @@
@NonNull
@Override
public CharSequence getTextAt(@NonNull Resources resources, long dateTimeMillis) {
+ if (mStringExpression != null && mTimeDependentText == null && mSurroundingText == null) {
+ throw new UnsupportedOperationException("getTextAt not supported for "
+ + "StringExpressions");
+ }
+
if (mTimeDependentText == null) {
return mSurroundingText;
}
@@ -522,7 +582,8 @@
*/
@NonNull
public static ComplicationText plainText(@NonNull CharSequence text) {
- return new ComplicationText(text, null);
+ return new ComplicationText(
+ text, /* timeDependentText= */ null, /* stringExpression= */ null);
}
/**
@@ -703,7 +764,8 @@
mReferencePeriodEndMillis,
mStyle,
showNowText,
- mMinimumUnit));
+ mMinimumUnit),
+ /* stringExpression= */ null);
}
/** Returns the default value for the 'show now text' option for the given {@code style}. */
@@ -791,7 +853,8 @@
@SuppressLint("SyntheticAccessor")
public ComplicationText build() {
return new ComplicationText(
- mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone));
+ mSurroundingText, new TimeFormatText(mFormat, mStyle, mTimeZone),
+ /* stringExpression= */ null);
}
}
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index d774957..4b5c05a 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -910,7 +910,7 @@
* The value may be accompanied by an icon and/or short text and title.
*
* The [min] and [max] fields are required for this type, as well as one of [value] or
- * [dynamicValue]. The value within the range is expected to always be displayed.
+ * [valueExpression]. The value within the range is expected to always be displayed.
*
* The icon, title, and text fields are optional and the watch face may choose which of these
* fields to display, if any.
@@ -925,9 +925,6 @@
* [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
* than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
* by the renderer. The semantic meaning of value is described by [valueType].
- * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
- * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
- * ignore this field.
* @property min The minimum [Float] value for this complication.
* @property max The maximum [Float] value for this complication.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -964,9 +961,7 @@
public class RangedValueComplicationData internal constructor(
public val value: Float,
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @ComplicationExperimental
- @get:ComplicationExperimental
- public val dynamicValue: DynamicFloat?,
+ valueExpression: FloatExpression?,
public val min: Float,
public val max: Float,
public val monochromaticImage: MonochromaticImage?,
@@ -991,6 +986,15 @@
persistencePolicy = persistencePolicy,
displayPolicy = displayPolicy
) {
+ /**
+ * The [FloatExpression] optionally set by the data source. If present the system will
+ * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
+ * this field.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public val valueExpression: FloatExpression? = valueExpression
+
/** @hide */
@IntDef(value = [TYPE_UNDEFINED, TYPE_RATING, TYPE_PERCENTAGE])
public annotation class RangedValueType
@@ -999,14 +1003,14 @@
* Builder for [RangedValueComplicationData].
*
* You must at a minimum set the [min], [max] and [contentDescription] fields, at least one of
- * [value] or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text]
+ * [value] or [valueExpression], and at least one of [monochromaticImage], [smallImage], [text]
* or [title].
*/
public class Builder
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public constructor(
private val value: Float,
- private val dynamicValue: DynamicFloat?,
+ private val valueExpression: FloatExpression?,
private val min: Float,
private val max: Float,
private var contentDescription: ComplicationText
@@ -1026,26 +1030,27 @@
min: Float,
max: Float,
contentDescription: ComplicationText
- ) : this(value, dynamicValue = null, min, max, contentDescription)
+ ) : this(value, valueExpression = null, min, max, contentDescription)
/**
- * Creates a [Builder] for a [RangedValueComplicationData] with a [DynamicFloat] value.
+ * Creates a [Builder] for a [RangedValueComplicationData] with a [FloatExpression] value.
*
- * @param dynamicValue The [DynamicFloat] of the ranged complication which will be evaluated
- * into a value dynamically, and should be in the range [[min]] .. [[max]]. The semantic
- * meaning of value can be specified via [setValueType].
+ * @param valueExpression The [FloatExpression] of the ranged complication which will be
+ * evaluated into a value dynamically, and should be in the range [[min]] .. [[max]]. The
+ * semantic meaning of value can be specified via [setValueType].
* @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
* @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
* [TYPE_PERCENTAGE] this must be 0f.
* @param contentDescription Localized description for use by screen readers
+ * @hide
*/
- @ComplicationExperimental
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public constructor(
- dynamicValue: DynamicFloat,
+ valueExpression: FloatExpression,
min: Float,
max: Float,
contentDescription: ComplicationText
- ) : this(value = min /* sensible default */, dynamicValue, min, max, contentDescription)
+ ) : this(value = min /* sensible default */, valueExpression, min, max, contentDescription)
private var tapAction: PendingIntent? = null
private var validTimeRange: TimeRange? = null
@@ -1125,7 +1130,7 @@
}
return RangedValueComplicationData(
value,
- dynamicValue,
+ valueExpression,
min,
max,
monochromaticImage,
@@ -1158,7 +1163,7 @@
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
- builder.setRangedDynamicValue(dynamicValue)
+ builder.setRangedValueExpression(valueExpression)
builder.setRangedMinValue(min)
builder.setRangedMaxValue(max)
monochromaticImage?.addToWireComplicationData(builder)
@@ -1188,7 +1193,7 @@
other as RangedValueComplicationData
if (value != other.value) return false
- if (dynamicValue != other.dynamicValue) return false
+ if (valueExpression != other.valueExpression) return false
if (valueType != other.valueType) return false
if (min != other.min) return false
if (max != other.max) return false
@@ -1210,7 +1215,7 @@
override fun hashCode(): Int {
var result = value.hashCode()
- result = 31 * result + (dynamicValue?.hashCode() ?: 0)
+ result = 31 * result + (valueExpression?.hashCode() ?: 0)
result = 31 * result + valueType
result = 31 * result + min.hashCode()
result = 31 * result + max.hashCode()
@@ -1235,13 +1240,13 @@
} else {
value.toString()
}
- val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+ val valueExpressionString = if (WireComplicationData.shouldRedact()) {
"REDACTED"
} else {
- dynamicValue.toString()
+ valueExpression.toString()
}
return "RangedValueComplicationData(value=$valueString, " +
- "dynamicValue=$dynamicValueString, valueType=$valueType, min=$min, " +
+ "valueExpression=$valueExpressionString, valueType=$valueType, min=$min, " +
"max=$max, monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
"title=$title, text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1309,7 +1314,7 @@
* text and title.
*
* The [targetValue] field is required for this type, as well as one of [value] or
- * [dynamicValue]. The progress is expected to always be displayed.
+ * [valueExpression]. The progress is expected to always be displayed.
*
* The icon, title, and text fields are optional and the watch face may choose which of these
* fields to display, if any.
@@ -1329,9 +1334,6 @@
* than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
* rather than rendering normally, its suggested to be drawn as a grey arc with a percentage value
* selected by the renderer.
- * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
- * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
- * ignore this field.
* @property targetValue The target [Float] value for this complication.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
* face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
@@ -1366,9 +1368,7 @@
internal constructor(
public val value: Float,
@Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @ComplicationExperimental
- @get:ComplicationExperimental
- public val dynamicValue: DynamicFloat?,
+ valueExpression: FloatExpression?,
public val targetValue: Float,
public val monochromaticImage: MonochromaticImage?,
public val smallImage: SmallImage?,
@@ -1392,10 +1392,19 @@
displayPolicy = displayPolicy
) {
/**
+ * The [FloatExpression] optionally set by the data source. If present the system will
+ * dynamically evaluate this and store the result in [value]. Watch faces can typically ignore
+ * this field.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public val valueExpression: FloatExpression? = valueExpression
+
+ /**
* Builder for [GoalProgressComplicationData].
*
* You must at a minimum set the [targetValue] and [contentDescription] fields, one of [value]
- * or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text] or
+ * or [valueExpression], and at least one of [monochromaticImage], [smallImage], [text] or
* [title].
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@@ -1403,7 +1412,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public constructor(
private val value: Float,
- private val dynamicValue: DynamicFloat?,
+ private val valueExpression: FloatExpression?,
private val targetValue: Float,
private var contentDescription: ComplicationText
) : BaseBuilder<Builder, GoalProgressComplicationData>() {
@@ -1418,24 +1427,25 @@
value: Float,
targetValue: Float,
contentDescription: ComplicationText
- ) : this(value, dynamicValue = null, targetValue, contentDescription)
+ ) : this(value, valueExpression = null, targetValue, contentDescription)
/**
- * Creates a [Builder] for a [GoalProgressComplicationData] with a [DynamicFloat] value.
+ * Creates a [Builder] for a [GoalProgressComplicationData] with a [FloatExpression] value.
*
- * @param dynamicValue The [DynamicFloat] of the goal complication which will be evaluated
- * into a value dynamically, and should be >= 0.
+ * @param valueExpression The [FloatExpression] of the goal complication which will be
+ * evaluated into a value dynamically, and should be >= 0.
* @param targetValue The target value. This must be less than [Float.MAX_VALUE].
* @param contentDescription Localized description for use by screen readers
+ * @hide
*/
- @ComplicationExperimental
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public constructor(
- dynamicValue: DynamicFloat,
+ valueExpression: FloatExpression,
targetValue: Float,
contentDescription: ComplicationText
) : this(
value = 0f /* sensible default */,
- dynamicValue,
+ valueExpression,
targetValue,
contentDescription
)
@@ -1502,7 +1512,7 @@
}
return GoalProgressComplicationData(
value,
- dynamicValue,
+ valueExpression,
targetValue,
monochromaticImage,
smallImage,
@@ -1533,7 +1543,7 @@
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
- builder.setRangedDynamicValue(dynamicValue)
+ builder.setRangedValueExpression(valueExpression)
builder.setTargetValue(targetValue)
monochromaticImage?.addToWireComplicationData(builder)
smallImage?.addToWireComplicationData(builder)
@@ -1561,7 +1571,7 @@
other as GoalProgressComplicationData
if (value != other.value) return false
- if (dynamicValue != other.dynamicValue) return false
+ if (valueExpression != other.valueExpression) return false
if (targetValue != other.targetValue) return false
if (monochromaticImage != other.monochromaticImage) return false
if (smallImage != other.smallImage) return false
@@ -1581,7 +1591,7 @@
override fun hashCode(): Int {
var result = value.hashCode()
- result = 31 * result + (dynamicValue?.hashCode() ?: 0)
+ result = 31 * result + (valueExpression?.hashCode() ?: 0)
result = 31 * result + targetValue.hashCode()
result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
result = 31 * result + (smallImage?.hashCode() ?: 0)
@@ -1604,13 +1614,13 @@
} else {
value.toString()
}
- val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+ val valueExpressionString = if (WireComplicationData.shouldRedact()) {
"REDACTED"
} else {
- dynamicValue.toString()
+ valueExpression.toString()
}
return "GoalProgressComplicationData(value=$valueString, " +
- "dynamicValue=$dynamicValueString, targetValue=$targetValue, " +
+ "valueExpression=$valueExpressionString, targetValue=$targetValue, " +
"monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
"text=$text, contentDescription=$contentDescription), " +
"tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -2669,7 +2679,7 @@
RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
value = rangedValue,
- dynamicValue = rangedDynamicValue,
+ valueExpression = rangedValueExpression,
min = rangedMinValue,
max = rangedMaxValue,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
@@ -2728,7 +2738,7 @@
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
GoalProgressComplicationData.Builder(
value = rangedValue,
- dynamicValue = rangedDynamicValue,
+ valueExpression = rangedValueExpression,
targetValue = targetValue,
contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
).apply {
@@ -2846,7 +2856,7 @@
RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
value = rangedValue,
- dynamicValue = rangedDynamicValue,
+ valueExpression = rangedValueExpression,
min = rangedMinValue,
max = rangedMaxValue,
contentDescription = contentDescription?.toApiComplicationText()
@@ -2922,7 +2932,7 @@
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
GoalProgressComplicationData.Builder(
value = rangedValue,
- dynamicValue = rangedDynamicValue,
+ valueExpression = rangedValueExpression,
targetValue = targetValue,
contentDescription = contentDescription?.toApiComplicationText()
?: ComplicationText.EMPTY
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
similarity index 69%
rename from wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
rename to wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
index 0374f72..6ac63a8 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/FloatExpression.kt
@@ -18,27 +18,32 @@
import androidx.annotation.RestrictTo
-/** Placeholder for DynamicFloat implementation by tiles. */
+/**
+ * Placeholder for FloatExpression implementation by tiles.
+ * @hide
+ */
// TODO(b/257413268): Replace this with the real implementation.
-@ComplicationExperimental
-abstract class DynamicFloat {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+abstract class FloatExpression {
abstract fun asByteArray(): ByteArray
override fun equals(other: Any?): Boolean {
if (this === other) return true
// Not checking for exact same class because it's not implemented yet.
- if (other !is DynamicFloat) return false
+ if (other !is FloatExpression) return false
return asByteArray().contentEquals(other.asByteArray())
}
override fun hashCode() = asByteArray().contentHashCode()
- override fun toString() = "DynamicFloatPlaceholder${asByteArray().contentToString()}"
+ override fun toString() = "FloatExpressionPlaceholder${asByteArray().contentToString()}"
}
-/** Placeholder parser for [DynamicFloat] from [ByteArray]. */
-@ComplicationExperimental
+/**
+ * Placeholder parser for [FloatExpression] from [ByteArray].
+ * @hide
+ */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-fun ByteArray.toDynamicFloat() = object : DynamicFloat() {
- override fun asByteArray() = this@toDynamicFloat
+fun ByteArray.toFloatExpression() = object : FloatExpression() {
+ override fun asByteArray() = this@toFloatExpression
}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
index f335e6a..f318733 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Text.kt
@@ -564,13 +564,9 @@
return false
}
- override fun hashCode(): Int {
- return delegate.hashCode()
- }
+ override fun hashCode() = delegate.hashCode()
- override fun toString(): String {
- return delegate.toString()
- }
+ override fun toString() = delegate.toString()
}
/** Converts a [WireComplicationText] into an equivalent [ComplicationText] instead. */
@@ -627,16 +623,92 @@
return true
}
- override fun hashCode(): Int {
- return delegate.hashCode()
- }
+ override fun hashCode() = delegate.hashCode()
- override fun toString(): String {
- return delegate.toString()
- }
+ override fun toString() = delegate.toString()
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun WireTimeDependentText.toApiComplicationText(): ComplicationText =
DelegatingTimeDependentText(this)
+
+/**
+ * This is a placeholder for the tiles StringExpression which isn't currently available. We'll
+ * remove this later in favor of the real thing.
+ * @hide
+ */
+// TODO(b/260065006): Remove this in favor of the real thing when available.
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class StringExpression(private val expression: ByteArray) {
+ fun asByteArray() = expression
+
+ override fun toString(): String {
+ return "StringExpression(expression=${expression.contentToString()})"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as StringExpression
+
+ if (!expression.contentEquals(other.expression)) return false
+
+ return true
+ }
+
+ override fun hashCode() = expression.contentHashCode()
+}
+
+/**
+ * A [ComplicationText] where the system evaluates a [StringExpression] on behalf of the watch face.
+ * By the time this reaches the watch face's Renderer, it'll have been converted to a plain
+ * ComplicationText.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class StringExpressionComplicationText(
+ public val expression: StringExpression
+) : ComplicationText {
+ private val delegate = DelegatingComplicationText(WireComplicationText(expression))
+
+ override fun getTextAt(resources: Resources, instant: Instant) =
+ delegate.getTextAt(resources, instant)
+
+ override fun returnsSameText(firstInstant: Instant, secondInstant: Instant) =
+ delegate.returnsSameText(firstInstant, secondInstant)
+
+ override fun getNextChangeTime(afterInstant: Instant): Instant =
+ delegate.getNextChangeTime(afterInstant)
+
+ override fun isAlwaysEmpty() = delegate.isAlwaysEmpty()
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun isPlaceholder(): Boolean = delegate.isPlaceholder()
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.SUBCLASSES)
+ override fun getTimeDependentText() = delegate.getTimeDependentText()
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun toWireComplicationText() = delegate.toWireComplicationText()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as StringExpressionComplicationText
+
+ if (delegate != other.delegate) return false
+
+ return true
+ }
+
+ override fun hashCode() = delegate.hashCode()
+
+ override fun toString() = delegate.toString()
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index 4494346..fefb8be 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -27,7 +27,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.wear.watchface.complications.data.ComplicationExperimental
import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
-import androidx.wear.watchface.complications.data.toDynamicFloat
+import androidx.wear.watchface.complications.data.toFloatExpression
import com.google.common.truth.Truth
import org.junit.Assert
import org.junit.Assert.assertThrows
@@ -97,7 +97,7 @@
// WHEN the relevant getters are called on the resulting data
// THEN the correct values are returned.
Assert.assertEquals(data.rangedValue, 57f, 0f)
- Assert.assertNull(data.rangedDynamicValue)
+ Assert.assertNull(data.rangedValueExpression)
Assert.assertEquals(data.rangedMinValue, 5f, 0f)
Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
@@ -111,7 +111,7 @@
// GIVEN complication data of the RANGED_VALUE type created by the Builder...
val data =
ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
- .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedMinValue(5f)
.setRangedMaxValue(150f)
.setShortTitle(ComplicationText.plainText("title"))
@@ -120,7 +120,7 @@
// WHEN the relevant getters are called on the resulting data
// THEN the correct values are returned.
- Truth.assertThat(data.rangedDynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+ Truth.assertThat(data.rangedValueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
Assert.assertEquals(data.rangedMinValue, 5f, 0f)
Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
index 2a85fef..75dda2c 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationTextTest.kt
@@ -21,6 +21,7 @@
import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
import androidx.test.core.app.ApplicationProvider
+import androidx.wear.watchface.complications.data.StringExpression
import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
import com.google.common.truth.Truth
import org.junit.Assert
@@ -799,4 +800,30 @@
Truth.assertThat(text.getNextChangeTime(600000123))
.isEqualTo(600060000)
}
-}
\ No newline at end of file
+
+ @Test
+ public fun stringExpressionToParcelRoundTrip() {
+ val text = ComplicationText(StringExpression(byteArrayOf(1, 2, 3)))
+
+ Truth.assertThat(text.toParcelRoundTrip()).isEqualTo(text)
+ }
+
+ @Test
+ public fun getTextAt_ignoresStringExpressionIfSurroundingStringPresent() {
+ val text = ComplicationText(
+ "hello" as CharSequence,
+ /* timeDependentText = */ null,
+ StringExpression(byteArrayOf(1, 2, 3))
+ )
+
+ Truth.assertThat(text.getTextAt(mResources, 132456789).toString())
+ .isEqualTo("hello")
+ }
+}
+
+fun ComplicationText.toParcelRoundTrip(): ComplicationText {
+ val parcel = Parcel.obtain()
+ writeToParcel(parcel, /* flags = */ 0)
+ parcel.setDataPosition(0)
+ return ComplicationText.CREATOR.createFromParcel(parcel)
+}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 44c76f8..14e3a3d 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -84,9 +84,9 @@
assertThat(data).isEqualTo(NoDataComplicationData())
assertThat(data.hashCode()).isEqualTo(NoDataComplicationData().hashCode())
assertThat(data.toString()).isEqualTo(
- "NoDataComplicationData(placeholder=null, " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+ "NoDataComplicationData(placeholder=null, tapActionLostDueToSerialization=false," +
+ " tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
"+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, displayPolicy=0)"
)
}
@@ -178,13 +178,14 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
- "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
- "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
- " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, persistencePolicy=1, displayPolicy=1)"
+ "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+ "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=1, displayPolicy=1)"
)
}
@@ -251,13 +252,14 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"ShortTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
- "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
- "mTimeDependentText=null}, monochromaticImage=MonochromaticImage(" +
- "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
- "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
- " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+ "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
+ "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
+ "type=PHOTO, ambientImage=null), contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
@@ -313,13 +315,13 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
- "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
- "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=title, mTimeDependentText=null, mStringExpression=null}, " +
+ "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -387,15 +389,16 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"LongTextComplicationData(text=ComplicationText{mSurroundingText=text, " +
- "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=title, " +
- "mTimeDependentText=null}, monochromaticImage=MonochromaticImage(image=" +
- "Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(image=" +
- "Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
+ "mTimeDependentText=null, mStringExpression=null}, " +
+ "title=ComplicationText{mSurroundingText=title, mTimeDependentText=null, " +
+ "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+ "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
+ "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
"contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -428,7 +431,7 @@
assertThat(deserialized.max).isEqualTo(100f)
assertThat(deserialized.min).isEqualTo(0f)
assertThat(deserialized.value).isEqualTo(95f)
- assertThat(deserialized.dynamicValue).isNull()
+ assertThat(deserialized.valueExpression).isNull()
assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
.isEqualTo("content description")
assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
@@ -455,24 +458,23 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
- "valueType=0, min=0.0, max=100.0, " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+ "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
+ "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=battery, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
"displayPolicy=0)"
)
}
@Test
- public fun rangedValueComplicationData_withDynamicValue() {
+ public fun rangedValueComplicationData_withValueExpression() {
val data = RangedValueComplicationData.Builder(
- dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ valueExpression = byteArrayOf(42, 107).toFloatExpression(),
min = 5f,
max = 100f,
contentDescription = "content description".complicationText
@@ -483,7 +485,7 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedValue(5f) // min as a sensible default
.setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
.setRangedMinValue(5f)
@@ -499,7 +501,7 @@
val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
assertThat(deserialized.max).isEqualTo(100f)
assertThat(deserialized.min).isEqualTo(5f)
- assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+ assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
assertThat(deserialized.value).isEqualTo(5f) // min as a sensible default
assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
.isEqualTo("content description")
@@ -507,7 +509,7 @@
.isEqualTo("battery")
val sameData = RangedValueComplicationData.Builder(
- dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ valueExpression = byteArrayOf(42, 107).toFloatExpression(),
min = 5f,
max = 100f,
contentDescription = "content description".complicationText
@@ -524,8 +526,8 @@
.setTitle("battery".complicationText)
.setDataSource(dataSourceA)
.build()
- val diffDataDynamicValue = RangedValueComplicationData.Builder(
- dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+ val diffDataValueExpression = RangedValueComplicationData.Builder(
+ valueExpression = byteArrayOf(43, 108).toFloatExpression(),
min = 5f,
max = 100f,
contentDescription = "content description".complicationText
@@ -536,26 +538,97 @@
assertThat(data).isEqualTo(sameData)
assertThat(data).isNotEqualTo(diffDataFixedValue)
- assertThat(data).isNotEqualTo(diffDataDynamicValue)
+ assertThat(data).isNotEqualTo(diffDataValueExpression)
assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
assertThat(data.hashCode()).isNotEqualTo(diffDataFixedValue.hashCode())
- assertThat(data.hashCode()).isNotEqualTo(diffDataDynamicValue.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(diffDataValueExpression.hashCode())
assertThat(data.toString()).isEqualTo(
"RangedValueComplicationData(value=5.0, " +
- "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
- "valueType=0, min=5.0, max=100.0, " +
- "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "valueExpression=FloatExpressionPlaceholder[42, 107], valueType=0, min=5.0, " +
+ "max=100.0, monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+ "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+ "text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
"ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
"displayPolicy=0)"
)
}
+ @Test
+ public fun rangedValueComplicationData_withStringExpression() {
+ val data = RangedValueComplicationData.Builder(
+ value = 95f, min = 0f, max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle(
+ StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+ )
+ .setDataSource(dataSourceA)
+ .build()
+ ParcelableSubject.assertThat(data.asWireComplicationData())
+ .hasSameSerializationAs(
+ WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+ .setRangedValue(95f)
+ .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
+ .setRangedMinValue(0f)
+ .setRangedMaxValue(100f)
+ .setShortTitle(
+ WireComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+ )
+ .setContentDescription(WireComplicationText.plainText("content description"))
+ .setDataSource(dataSourceA)
+ .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+ .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+ .build()
+ )
+ testRoundTripConversions(data)
+ val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
+ assertThat(deserialized.max).isEqualTo(100f)
+ assertThat(deserialized.min).isEqualTo(0f)
+ assertThat(deserialized.value).isEqualTo(95f)
+ assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+ .isEqualTo("content description")
+
+ val sameData = RangedValueComplicationData.Builder(
+ value = 95f, min = 0f, max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle(
+ StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 3, 4, 5)))
+ )
+ .setDataSource(dataSourceA)
+ .build()
+
+ val differentData = RangedValueComplicationData.Builder(
+ value = 95f, min = 0f, max = 100f,
+ contentDescription = "content description".complicationText
+ )
+ .setTitle(StringExpressionComplicationText(StringExpression(byteArrayOf(1, 2, 4, 5))))
+ .setDataSource(dataSourceB)
+ .build()
+
+ assertThat(data).isEqualTo(sameData)
+ assertThat(data).isNotEqualTo(differentData)
+ assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+ assertThat(data.hashCode()).isNotEqualTo(differentData.hashCode())
+ assertThat(data.toString()).isEqualTo(
+ "RangedValueComplicationData(value=95.0, valueExpression=null, valueType=0, " +
+ "min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=(null), mTimeDependentText=null, " +
+ "mStringExpression=StringExpression(expression=[1, 2, 3, 4, 5])}, text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+ "displayPolicy=0)"
+ )
+ }
+
@RequiresApi(Build.VERSION_CODES.P)
@Test
public fun rangedValueComplicationData_withImages() {
@@ -626,15 +699,16 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+ "RangedValueComplicationData(value=95.0, valueExpression=null, " +
"valueType=1, min=0.0, max=100.0, " +
"monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
"ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
"type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
- " mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ " mTimeDependentText=null, mStringExpression=null}, text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
"displayPolicy=0)"
@@ -665,7 +739,7 @@
testRoundTripConversions(data)
val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
assertThat(deserialized.value).isEqualTo(1200f)
- assertThat(deserialized.dynamicValue).isNull()
+ assertThat(deserialized.valueExpression).isNull()
assertThat(deserialized.targetValue).isEqualTo(10000f)
assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
.isEqualTo("content description")
@@ -693,24 +767,23 @@
assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
"targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
- "text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
- "persistencePolicy=0, displayPolicy=0)"
+ "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+ "displayPolicy=0)"
)
}
@Test
- public fun goalProgressComplicationData_withDynamicValue() {
+ public fun goalProgressComplicationData_withValueExpression() {
val data = GoalProgressComplicationData.Builder(
- dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ valueExpression = byteArrayOf(42, 107).toFloatExpression(),
targetValue = 10000f,
contentDescription = "content description".complicationText
)
@@ -720,7 +793,7 @@
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
- .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedValue(0f) // sensible default
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -732,7 +805,7 @@
)
testRoundTripConversions(data)
val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
- assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+ assertThat(deserialized.valueExpression!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
assertThat(deserialized.value).isEqualTo(0f) // sensible default
assertThat(deserialized.targetValue).isEqualTo(10000f)
assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
@@ -741,7 +814,7 @@
.isEqualTo("steps")
val sameData = GoalProgressComplicationData.Builder(
- dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+ valueExpression = byteArrayOf(42, 107).toFloatExpression(),
targetValue = 10000f,
contentDescription = "content description".complicationText
)
@@ -750,7 +823,7 @@
.build()
val diffData = GoalProgressComplicationData.Builder(
- dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+ valueExpression = byteArrayOf(43, 108).toFloatExpression(),
targetValue = 10000f,
contentDescription = "content description".complicationText
)
@@ -763,18 +836,17 @@
assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=0.0, " +
- "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
- "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
- "text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, colorRamp=null, " +
- "persistencePolicy=0, displayPolicy=0)"
+ "GoalProgressComplicationData(value=0.0, valueExpression=" +
+ "FloatExpressionPlaceholder[42, 107], targetValue=10000.0, " +
+ "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+ "mSurroundingText=steps, mTimeDependentText=null, mStringExpression=null}, " +
+ "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
+ "description, mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+ "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
+ "displayPolicy=0)"
)
}
@@ -834,17 +906,16 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
"targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
- "text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, -16711936, " +
- "-16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
+ "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+ "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
)
}
@@ -917,19 +988,19 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+ "GoalProgressComplicationData(value=1200.0, valueExpression=null, " +
"targetValue=10000.0, " +
"monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
"ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
"type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=steps, " +
- "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
- "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
- "persistencePolicy=0, displayPolicy=0)"
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+ "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
)
}
@@ -992,17 +1063,17 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+ "RangedValueComplicationData(value=95.0, valueExpression=null, " +
"valueType=0, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=ComplicationText{" +
- "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, -16711936, " +
- "-16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
+ "mSurroundingText=battery, mTimeDependentText=null, mStringExpression=null}, " +
+ "text=null, contentDescription=ComplicationText{mSurroundingText=content " +
+ "description, mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+ "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1094,13 +1165,12 @@
"WeightedElementsComplicationData(elements=Element(color=-65536, weight=0.5)," +
" Element(color=-16711936, weight=1.0), Element(color=-16776961, weight=2.0), " +
"elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null}, " +
- "text=null, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1193,12 +1263,12 @@
"image=Icon(typ=URI uri=someuri), ambientImage=null), " +
"smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
"ambientImage=null), title=ComplicationText{mSurroundingText=calories, " +
- "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
+ "mTimeDependentText=null, mStringExpression=null}, text=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
"tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1239,12 +1309,12 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "MonochromaticImageComplicationData(monochromaticImage=MonochromaticImage(image=" +
- "Icon(typ=URI uri=someuri), ambientImage=null), contentDescription=" +
- "ComplicationText{mSurroundingText=content description, mTimeDependentText=null})" +
- ", tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "MonochromaticImageComplicationData(monochromaticImage=MonochromaticImage(" +
+ "image=Icon(typ=URI uri=someuri), ambientImage=null), contentDescription=" +
+ "ComplicationText{mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1290,12 +1360,13 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "SmallImageComplicationData(smallImage=SmallImage(image=Icon(typ=URI uri=someuri)" +
- ", type=PHOTO, ambientImage=null), contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "SmallImageComplicationData(smallImage=SmallImage(image=Icon(" +
+ "typ=URI uri=someuri), type=PHOTO, ambientImage=null), " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1372,11 +1443,12 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "PhotoImageComplicationData(photoImage=Icon(typ=URI uri=someuri), contentDescription=" +
- "ComplicationText{mSurroundingText=content description, mTimeDependentText=null})" +
- ", tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "PhotoImageComplicationData(photoImage=Icon(typ=URI uri=someuri), " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1416,12 +1488,13 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=needs location," +
- " mTimeDependentText=null}, title=null, monochromaticImage=null, smallImage=null," +
- " tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
+ "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=needs location, " +
+ "mTimeDependentText=null, mStringExpression=null}, title=null, " +
+ "monochromaticImage=null, smallImage=null, tapActionLostDueToSerialization=false," +
+ " tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
+ "+1000000000-12-31T23:59:59.999999999Z), dataSource=ComponentInfo{" +
+ "com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
}
@@ -1467,12 +1540,13 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "NoPermissionComplicationData(text=ComplicationText{mSurroundingText=" +
- "needs location, mTimeDependentText=null}, title=null, monochromaticImage=" +
- "MonochromaticImage(image=Icon(typ=URI uri=someuri), ambientImage=null), " +
- "smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), type=PHOTO, " +
- "ambientImage=null), tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "NoPermissionComplicationData(text=ComplicationText{" +
+ "mSurroundingText=needs location, mTimeDependentText=null, " +
+ "mStringExpression=null}, title=null, monochromaticImage=MonochromaticImage(" +
+ "image=Icon(typ=URI uri=someuri), ambientImage=null), smallImage=SmallImage(" +
+ "image=Icon(typ=URI uri=someuri2), type=PHOTO, ambientImage=null), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+ "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, displayPolicy=0)"
)
@@ -1539,18 +1613,19 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=ShortTextComplicationData(text=" +
- "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
- "title=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText" +
- "=null}, monochromaticImage=MonochromaticImage(image=Icon(typ=RESOURCE pkg= " +
- "id=0xffffffff), ambientImage=null), smallImage=null, " +
- "contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}, " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+ "mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+ "mStringExpression=null}, monochromaticImage=MonochromaticImage(" +
+ "image=Icon(typ=RESOURCE pkg= id=0xffffffff), ambientImage=null), " +
+ "smallImage=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}, tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
- "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null," +
- " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
"displayPolicy=0)"
)
@@ -1605,13 +1680,14 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "NoDataComplicationData(placeholder=LongTextComplicationData(text=" +
- "ComplicationText{mSurroundingText=text, mTimeDependentText=null}, title=null, " +
- "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "NoDataComplicationData(placeholder=LongTextComplicationData(" +
+ "text=ComplicationText{mSurroundingText=text, mTimeDependentText=null, " +
+ "mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
"validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -1688,13 +1764,13 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, dynamicValue=null, valueType=0, min=0.0, max=100.0, " +
+ "value=3.4028235E38, valueExpression=null, valueType=0, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
- "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=" +
- "content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+ "mStringExpression=null}, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=null, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
@@ -1771,19 +1847,19 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=GoalProgressComplicationData(" +
- "value=3.4028235E38, dynamicValue=null, targetValue=10000.0, " +
+ "value=3.4028235E38, valueExpression=null, targetValue=10000.0, " +
"monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
- "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=content description, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+ "mStringExpression=null}, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, " +
- "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=false)," +
- " persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=false), " +
+ "persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
+ "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
+ "-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
"displayPolicy=0)"
)
@@ -1866,12 +1942,12 @@
"NoDataComplicationData(placeholder=WeightedElementsComplicationData(" +
"elements=Element(color=-65536, weight=0.5), Element(color=-16711936, " +
"weight=1.0), Element(color=-16776961, weight=2.0), " +
- "elementBackgroundColor=-7829368, monochromaticImage=null, " +
- "smallImage=null, title=ComplicationText{mSurroundingText=calories, " +
- "mTimeDependentText=null}, text=null, contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "elementBackgroundColor=-7829368, monochromaticImage=null, smallImage=null, " +
+ "title=ComplicationText{mSurroundingText=calories, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
@@ -1955,19 +2031,18 @@
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
"NoDataComplicationData(placeholder=RangedValueComplicationData(" +
- "value=3.4028235E38, dynamicValue=null, valueType=1, min=0.0, max=100.0, " +
+ "value=3.4028235E38, valueExpression=null, valueType=1, min=0.0, max=100.0, " +
"monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
- "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=" +
- "content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "mSurroundingText=__placeholder__, mTimeDependentText=null, " +
+ "mStringExpression=null}, contentDescription=ComplicationText{" +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
- "dataSource=ComponentInfo{com.pkg_a/com.a}, " +
- "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
- "persistencePolicy=0, displayPolicy=0), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "dataSource=ComponentInfo{com.pkg_a/com.a}, colorRamp=ColorRamp(colors=[-65536, " +
+ "-16711936, -16776961], interpolated=true), persistencePolicy=0, " +
+ "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
"endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), persistencePolicy=0, " +
"displayPolicy=0)"
)
@@ -2027,10 +2102,10 @@
"NoDataComplicationData(placeholder=MonochromaticImageComplicationData(" +
"monochromaticImage=MonochromaticImage(image=Icon(typ=RESOURCE pkg= " +
"id=0xffffffff), ambientImage=null), contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "mSurroundingText=content description, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
"validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2094,10 +2169,10 @@
"NoDataComplicationData(placeholder=SmallImageComplicationData(smallImage=" +
"SmallImage(image=Icon(typ=RESOURCE pkg= id=0xffffffff), type=ICON, " +
"ambientImage=null), contentDescription=ComplicationText{mSurroundingText=" +
- "content description, mTimeDependentText=null}), tapActionLostDueToSerialization=" +
- "false, tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
- "-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "content description, mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
+ "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
"validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2156,12 +2231,13 @@
assertThat(data.hashCode()).isEqualTo(data2.hashCode())
assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
assertThat(data.toString()).isEqualTo(
- "NoDataComplicationData(placeholder=PhotoImageComplicationData(photoImage=" +
- "Icon(typ=RESOURCE pkg= id=0xffffffff), contentDescription=ComplicationText{" +
- "mSurroundingText=content description, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, endDateTimeMillis=" +
- "+1000000000-12-31T23:59:59.999999999Z), " +
+ "NoDataComplicationData(placeholder=PhotoImageComplicationData(" +
+ "photoImage=Icon(typ=RESOURCE pkg= id=0xffffffff), " +
+ "contentDescription=ComplicationText{mSurroundingText=content description, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
+ "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), " +
"dataSource=ComponentInfo{com.pkg_a/com.a}, persistencePolicy=0, " +
"displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
"validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
@@ -2267,10 +2343,10 @@
}
@Test
- public fun rangedValueComplicationData_withDynamicValue() {
+ public fun rangedValueComplicationData_withValueExpression() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setRangedMinValue(0f)
.setRangedMaxValue(100f)
.setShortTitle(WireComplicationText.plainText("battery"))
@@ -2316,10 +2392,10 @@
}
@Test
- public fun goalProgressComplicationData_withDynamicValue() {
+ public fun goalProgressComplicationData_withValueExpression() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
- .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+ .setRangedValueExpression(byteArrayOf(42, 107).toFloatExpression())
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
.setContentDescription(WireComplicationText.plainText("content description"))
@@ -3251,11 +3327,12 @@
assertThat(data.toString()).isEqualTo(
"ShortTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}, title=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}, monochromaticImage=null, smallImage=null, " +
- "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}, tapActionLostDueToSerialization=false, tapAction=null," +
- " validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+ "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+ "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
"displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
@@ -3273,13 +3350,14 @@
.build()
assertThat(data.toString()).isEqualTo(
- "LongTextComplicationData(text=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
- "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
- "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
+ "LongTextComplicationData(text=ComplicationText{mSurroundingText=REDACTED, " +
+ "mTimeDependentText=null, mStringExpression=null}, title=ComplicationText{" +
+ "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}, " +
+ "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+ "displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
"ComplicationData{mType=4, mFields=REDACTED}"
@@ -3299,14 +3377,15 @@
.build()
assertThat(data.toString()).isEqualTo(
- "RangedValueComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+ "RangedValueComplicationData(value=REDACTED, valueExpression=REDACTED, " +
"valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
- "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
+ "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=ComplicationText{mSurroundingText=REDACTED, " +
+ "mTimeDependentText=null, mStringExpression=null}, contentDescription=" +
+ "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+ "mStringExpression=null}), tapActionLostDueToSerialization=false, tapAction=null," +
+ " validTimeRange=TimeRange(REDACTED), dataSource=null, colorRamp=null, " +
+ "persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
"ComplicationData{mType=5, mFields=REDACTED}"
@@ -3325,14 +3404,14 @@
.build()
assertThat(data.toString()).isEqualTo(
- "GoalProgressComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+ "GoalProgressComplicationData(value=REDACTED, valueExpression=REDACTED, " +
"targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
- "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
- "text=null, contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
- "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
- "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
- "persistencePolicy=0, displayPolicy=0)"
+ "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null, " +
+ "mStringExpression=null}, text=null, contentDescription=ComplicationText{" +
+ "mSurroundingText=REDACTED, mTimeDependentText=null, mStringExpression=null}), " +
+ "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
+ "TimeRange(REDACTED), dataSource=null, colorRamp=ColorRamp(colors=[-65536, " +
+ "-16711936, -16776961], interpolated=true), persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
"ComplicationData{mType=13, mFields=REDACTED}"
@@ -3349,13 +3428,14 @@
)
assertThat(data.toString()).isEqualTo(
- "NoDataComplicationData(placeholder=LongTextComplicationData(text=" +
- "ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
- "title=null, monochromaticImage=null, smallImage=null, contentDescription=" +
- "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
- "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=" +
- "TimeRange(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0), " +
+ "NoDataComplicationData(placeholder=LongTextComplicationData(" +
+ "text=ComplicationText{mSurroundingText=__placeholder__, mTimeDependentText=null," +
+ " mStringExpression=null}, title=null, monochromaticImage=null, smallImage=null, " +
+ "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+ "mTimeDependentText=null, mStringExpression=null}), " +
"tapActionLostDueToSerialization=false, tapAction=null, " +
+ "validTimeRange=TimeRange(REDACTED), dataSource=null, persistencePolicy=0, " +
+ "displayPolicy=0), tapActionLostDueToSerialization=false, tapAction=null, " +
"validTimeRange=TimeRange(REDACTED), persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
index b691df3..e16b877 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/TextTest.kt
@@ -238,7 +238,9 @@
ComplicationText.FORMAT_STYLE_DEFAULT,
null
)
- val text = TimeDifferenceComplicationText(ComplicationText("test", tft))
+ val text = TimeDifferenceComplicationText(
+ ComplicationText("test", tft, /* stringExpression = */ null)
+ )
assertNull(text.getMinimumTimeUnit())
}
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/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 1d98429..d5c8fff 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -684,15 +684,20 @@
}
/**
- * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
- * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
- * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
- * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
- * it returns `null`.
+ * When a UserStyleSchema contains hierarchical styles, only part of it is deemed to be active
+ * based on the user’s options in [userStyle]. Conversely if the UserStyleSchema doesn’t contain
+ * any hierarchical styles then all of it is considered to be active all the time.
+ *
+ * From the active portion of the UserStyleSchema we only allow there to be at most one
+ * [UserStyleSetting.ComplicationSlotsUserStyleSetting]. This function searches the active
+ * portion of the UserStyleSchema for the [UserStyleSetting.ComplicationSlotsUserStyleSetting],
+ * if one is found then it returns the selected [ComplicationSlotsOption] from that, based on
+ * the [userStyle]. If a [UserStyleSetting.ComplicationSlotsUserStyleSetting] is not found in
+ * the active portion of the UserStyleSchema it returns `null`.
*
* @param userStyle The [UserStyle] for which the function will search for the selected
* [ComplicationSlotsOption], if any.
- * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+ * @return The selected [ComplicationSlotsOption] based on the [userStyle] if any, or `null`
* otherwise.
*/
public fun findComplicationSlotsOptionForUserStyle(
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/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 5e8f737..2eb58be 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -47,6 +47,7 @@
import android.view.SurfaceHolder
import android.view.WindowInsets
import android.view.accessibility.AccessibilityManager
+import androidx.annotation.AnyThread
import androidx.annotation.Px
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
@@ -102,6 +103,8 @@
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -347,10 +350,20 @@
public val XML_WATCH_FACE_METADATA =
"androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
- internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
- engine: WatchFaceService.EngineWrapper?,
+ internal enum class ExecutionThread {
+ UI,
+ CURRENT
+ }
+ /**
+ * Waits for deferredValue using runBlocking, then executes the task on the thread
+ * specified by executionThread param.
+ */
+ private fun <R, V> awaitDeferredThenRunTaskOnThread(
+ engine: EngineWrapper?,
traceName: String,
- task: (watchFaceImpl: WatchFaceImpl) -> R
+ executionThread: ExecutionThread,
+ task: (deferredValue: V) -> R,
+ waitDeferred: suspend (engine: EngineWrapper) -> V
): R? = TraceEvent(traceName).use {
if (engine == null) {
Log.w(TAG, "Task $traceName posted after close(), ignoring.")
@@ -359,9 +372,16 @@
runBlocking {
try {
withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- val watchFaceImpl = engine.deferredWatchFaceImpl.await()
- withContext(engine.uiThreadCoroutineScope.coroutineContext) {
- task(watchFaceImpl)
+ val deferredValue = waitDeferred(engine)
+ when (executionThread) {
+ ExecutionThread.UI -> {
+ withContext(engine.uiThreadCoroutineScope.coroutineContext) {
+ task(deferredValue)
+ }
+ }
+ ExecutionThread.CURRENT -> {
+ task(deferredValue)
+ }
}
}
} catch (e: Exception) {
@@ -371,29 +391,24 @@
}
}
+ internal fun <R> awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
+ engine: EngineWrapper?,
+ traceName: String,
+ task: (watchFaceImpl: WatchFaceImpl) -> R
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+ it.deferredWatchFaceImpl.await()
+ }
+
/**
* During startup tasks will run before those posted by
* [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking].
*/
- internal fun <R> awaitDeferredWatchFaceThenRunOnBinderThread(
- engine: WatchFaceService.EngineWrapper?,
+ internal fun <R> awaitDeferredWatchFaceThenRunOnUiThread(
+ engine: EngineWrapper?,
traceName: String,
task: (watchFace: WatchFace) -> R
- ): R? = TraceEvent(traceName).use {
- if (engine == null) {
- Log.w(TAG, "Task $traceName posted after close(), ignoring.")
- return null
- }
- runBlocking {
- try {
- withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- task(engine.deferredWatchFace.await())
- }
- } catch (e: Exception) {
- Log.e(TAG, "Operation $traceName failed", e)
- throw e
- }
- }
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, ExecutionThread.UI, task) {
+ it.deferredWatchFace.await()
}
/**
@@ -401,25 +416,13 @@
* [awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking] and
* [awaitDeferredWatchFaceThenRunOnBinderThread].
*/
- internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine: WatchFaceService.EngineWrapper?,
+ internal fun <R> awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engine: EngineWrapper?,
traceName: String,
+ executionThread: ExecutionThread,
task: (earlyInitDetails: EarlyInitDetails) -> R
- ): R? = TraceEvent(traceName).use {
- if (engine == null) {
- Log.w(TAG, "Task $traceName ignored due to null engine.")
- return null
- }
- runBlocking {
- try {
- withTimeout(AWAIT_DEFERRED_TIMEOUT) {
- task(engine.deferredEarlyInitDetails.await())
- }
- } catch (e: Exception) {
- Log.e(TAG, "Operation $traceName failed", e)
- throw e
- }
- }
+ ): R? = awaitDeferredThenRunTaskOnThread(engine, traceName, executionThread, task) {
+ it.deferredEarlyInitDetails.await()
}
}
@@ -816,30 +819,31 @@
) = TraceEvent(
"WatchFaceService.writeComplicationCache"
).use {
- // File IO can be slow so perform the write from a background thread.
- getBackgroundThreadHandler().post {
- try {
- val stream = ByteArrayOutputStream()
- val objectOutputStream = ObjectOutputStream(stream)
- objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
- for (slot in complicationSlotsManager.complicationSlots) {
- objectOutputStream.writeInt(slot.key)
- objectOutputStream.writeObject(
- if ((slot.value.complicationData.value.persistencePolicy and
- ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
- ) {
- NoDataComplicationData().asWireComplicationData()
- } else {
- slot.value.complicationData.value.asWireComplicationData()
- }
- )
- }
- objectOutputStream.close()
- val byteArray = stream.toByteArray()
- writeComplicationDataCacheByteArray(context, fileName, byteArray)
- } catch (e: Exception) {
- Log.w(TAG, "Failed to write to complication cache due to exception", e)
+ try {
+ val stream = ByteArrayOutputStream()
+ val objectOutputStream = ObjectOutputStream(stream)
+ objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+ for (slot in complicationSlotsManager.complicationSlots) {
+ objectOutputStream.writeInt(slot.key)
+ objectOutputStream.writeObject(
+ if ((slot.value.complicationData.value.persistencePolicy and
+ ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+ ) {
+ NoDataComplicationData().asWireComplicationData()
+ } else {
+ slot.value.complicationData.value.asWireComplicationData()
+ }
+ )
}
+ objectOutputStream.close()
+ val byteArray = stream.toByteArray()
+
+ // File IO can be slow so perform the write from a background thread.
+ getBackgroundThreadHandler().post {
+ writeComplicationDataCacheByteArray(context, fileName, byteArray)
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to write to complication cache due to exception", e)
}
}
@@ -1134,8 +1138,7 @@
internal class EarlyInitDetails(
val complicationSlotsManager: ComplicationSlotsManager,
val userStyleRepository: CurrentUserStyleRepository,
- val userStyleFlavors: UserStyleFlavors,
- val surfaceHolder: SurfaceHolder
+ val userStyleFlavors: UserStyleFlavors
)
/** @hide */
@@ -1179,7 +1182,7 @@
* [deferredSurfaceHolder] will complete after [onSurfaceChanged], before then it's not
* safe to create a UiThread OpenGL context.
*/
- internal var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
+ private var deferredSurfaceHolder = CompletableDeferred<SurfaceHolder>()
internal val mutableWatchState = getMutableWatchState().apply {
isVisible.value = this@EngineWrapper.isVisible || forceIsVisibleForTesting()
@@ -1264,9 +1267,9 @@
private var asyncWatchFaceConstructionPending = false
- // Stores the initial ComplicationSlots which could get updated before they're applied.
- internal var pendingInitialComplications: List<IdAndComplicationDataWireFormat>? = null
- private set
+ @VisibleForTesting
+ internal val complicationsFlow =
+ MutableStateFlow<List<IdAndComplicationDataWireFormat>>(emptyList())
private var initialUserStyle: UserStyleWireFormat? = null
internal lateinit var interactiveInstanceId: String
@@ -1524,61 +1527,46 @@
}
}
- @UiThread
+ @AnyThread
internal fun setComplicationDataList(
complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
): Unit = TraceEvent("EngineWrapper.setComplicationDataList").use {
- val earlyInitDetails = getEarlyInitDetailsOrNull()
- if (earlyInitDetails != null) {
- applyComplications(
- earlyInitDetails.complicationSlotsManager,
- complicationDataWireFormats
- )
- } else {
- setPendingInitialComplications(complicationDataWireFormats)
- }
- }
-
- @UiThread
- internal fun applyComplications(
- complicationSlotsManager: ComplicationSlotsManager,
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
- for (idAndComplicationData in complicationDataWireFormats) {
- complicationSlotsManager.onComplicationDataUpdate(
- idAndComplicationData.id,
- idAndComplicationData.complicationData.toApiComplicationData(),
- now
- )
- }
- complicationSlotsManager.onComplicationsUpdated()
- invalidate()
- scheduleWriteComplicationDataCache()
- }
-
- @UiThread
- internal fun setPendingInitialComplications(
- complicationDataWireFormats: List<IdAndComplicationDataWireFormat>
- ) {
- // If the watchface hasn't been created yet, update pendingInitialComplications so
- // it can be applied later.
- if (pendingInitialComplications == null) {
- pendingInitialComplications = complicationDataWireFormats
- } else {
+ complicationsFlow.update { base ->
// We need to merge the updates.
- val complicationUpdateMap = pendingInitialComplications!!.associate {
+ val complicationUpdateMap = base.associate {
Pair(it.id, it.complicationData)
}.toMutableMap()
for (data in complicationDataWireFormats) {
complicationUpdateMap[data.id] = data.complicationData
}
- pendingInitialComplications = complicationUpdateMap.map {
+ complicationUpdateMap.map {
IdAndComplicationDataWireFormat(it.key, it.value)
}
}
}
+ @WorkerThread
+ private fun listenForComplicationChanges(
+ complicationSlotsManager: ComplicationSlotsManager
+ ) {
+ // Add a listener so we can track changes and automatically apply them on the UIThread
+ uiThreadCoroutineScope.launch {
+ complicationsFlow.collect { complicationDataWireFormats ->
+ val now = Instant.ofEpochMilli(systemTimeProvider.getSystemTimeMillis())
+ for (idAndComplicationData in complicationDataWireFormats) {
+ complicationSlotsManager.onComplicationDataUpdate(
+ idAndComplicationData.id,
+ idAndComplicationData.complicationData.toApiComplicationData(),
+ now
+ )
+ }
+ complicationSlotsManager.onComplicationsUpdated()
+ invalidate()
+ scheduleWriteComplicationDataCache()
+ }
+ }
+ }
+
@UiThread
internal suspend fun updateInstance(newInstanceId: String) {
val watchFaceImpl = deferredWatchFaceImpl.await()
@@ -1999,10 +1987,12 @@
// Store the initial complications, this could be modified by new data before being
// applied.
- pendingInitialComplications = params.idAndComplicationDataWireFormats
-
- if (pendingInitialComplications == null || pendingInitialComplications!!.isEmpty()) {
- pendingInitialComplications = readComplicationDataCache(_context, params.instanceId)
+ var initialComplications = params.idAndComplicationDataWireFormats
+ if (initialComplications.isNullOrEmpty()) {
+ initialComplications = readComplicationDataCache(_context, params.instanceId)
+ }
+ if (!initialComplications.isNullOrEmpty()) {
+ setComplicationDataList(initialComplications)
}
createWatchFaceInternal(
@@ -2038,6 +2028,15 @@
asyncWatchFaceConstructionPending = true
createdBy = _createdBy
+ // In case of overrideSurfaceHolder provided (tests) return its size instead of real
+ // metrics.
+ screenBounds = if (overrideSurfaceHolder != null) {
+ overrideSurfaceHolder.surfaceFrame
+ } else {
+ val displayMetrics = resources.displayMetrics
+ Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)
+ }
+
backgroundThreadCoroutineScope.launch {
val timeBefore = System.currentTimeMillis()
val currentUserStyleRepository =
@@ -2052,12 +2051,21 @@
}
complicationSlotsManager.watchState = watchState
complicationSlotsManager.listenForStyleChanges(uiThreadCoroutineScope)
+ listenForComplicationChanges(complicationSlotsManager)
val userStyleFlavors =
TraceEvent("WatchFaceService.createUserStyleFlavors").use {
createUserStyleFlavors(currentUserStyleRepository, complicationSlotsManager)
}
+ deferredEarlyInitDetails.complete(
+ EarlyInitDetails(
+ complicationSlotsManager,
+ currentUserStyleRepository,
+ userStyleFlavors
+ )
+ )
+
val deferredWatchFace = CompletableDeferred<WatchFace>()
val initComplicationsDone = CompletableDeferred<Unit>()
@@ -2076,24 +2084,6 @@
try {
val surfaceHolder = overrideSurfaceHolder ?: deferredSurfaceHolder.await()
- deferredEarlyInitDetails.complete(
- EarlyInitDetails(
- complicationSlotsManager,
- currentUserStyleRepository,
- userStyleFlavors,
- surfaceHolder
- )
- )
-
- withContext(uiThreadCoroutineScope.coroutineContext) {
- // Apply any pendingInitialComplications, this must be done after
- // deferredEarlyInitDetails has completed or there's a window in which complication
- // updates get lost.
- pendingInitialComplications?.let {
- applyComplications(complicationSlotsManager, it)
- }
- pendingInitialComplications = null
- }
val watchFace = TraceEvent("WatchFaceService.createWatchFace").use {
// Note by awaiting deferredSurfaceHolder we ensure onSurfaceChanged has
@@ -2678,6 +2668,9 @@
}
}
+ internal lateinit var screenBounds: Rect
+ private set
+
@UiThread
internal fun dump(writer: IndentingPrintWriter) {
require(uiThreadHandler.looper.isCurrentThread) {
@@ -2722,7 +2715,7 @@
writer.println("destroyed=$destroyed")
writer.println("surfaceDestroyed=$surfaceDestroyed")
writer.println(
- "pendingInitialComplications=" + pendingInitialComplications?.joinToString()
+ "lastComplications=" + complicationsFlow.value.joinToString()
)
synchronized(lock) {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index badd1a8..a5f3897 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -85,11 +85,14 @@
"HeadlessWatchFaceImpl.getPreviewReferenceTimeMillis"
) { it.previewReferenceInstant.toEpochMilli() } ?: 0
- override fun getComplicationState() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine,
- "HeadlessWatchFaceImpl.getComplicationState"
- ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+ override fun getComplicationState() = run {
+ val engineCopy = engine
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engineCopy,
+ "HeadlessWatchFaceImpl.getComplicationState",
+ WatchFaceService.Companion.ExecutionThread.UI
+ ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
+ }
override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
WatchFaceService.awaitDeferredWatchFaceImplThenRunOnUiThreadBlocking(
@@ -98,21 +101,24 @@
) { it.renderComplicationToBitmap(params) }
override fun getUserStyleSchema() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.getUserStyleSchema"
+ "HeadlessWatchFaceImpl.getUserStyleSchema",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.toWireFormat() }
override fun computeUserStyleSchemaDigestHash() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash"
+ "HeadlessWatchFaceImpl.computeUserStyleSchemaDigestHash",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.getDigestHash() }
override fun getUserStyleFlavors() =
- WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "HeadlessWatchFaceImpl.getUserStyleFlavors"
+ "HeadlessWatchFaceImpl.getUserStyleFlavors",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleFlavors.toWireFormat() }
override fun release() {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 1eae0b4..ba6fce5 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -87,7 +87,7 @@
}
override fun getWatchFaceOverlayStyle(): WatchFaceOverlayStyleWireFormat? =
- WatchFaceService.awaitDeferredWatchFaceThenRunOnBinderThread(
+ WatchFaceService.awaitDeferredWatchFaceThenRunOnUiThread(
engine,
"InteractiveWatchFaceImpl.getWatchFaceOverlayStyle"
) { WatchFaceOverlayStyleWireFormat(
@@ -160,9 +160,7 @@
override fun updateComplicationData(
complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
- ): Unit = uiThreadCoroutineScope.runBlockingWithTracing(
- "InteractiveWatchFaceImpl.updateComplicationData"
- ) {
+ ): Unit = TraceEvent("InteractiveWatchFaceImpl.updateComplicationData").use {
if ("user" != Build.TYPE) {
Log.d(TAG, "updateComplicationData " + complicationDatumWireFormats.joinToString())
}
@@ -191,16 +189,19 @@
}
override fun getComplicationDetails(): List<IdAndComplicationStateWireFormat>? {
- return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
- engine,
- "InteractiveWatchFaceImpl.getComplicationDetails"
- ) { it.complicationSlotsManager.getComplicationsState(it.surfaceHolder.surfaceFrame) }
+ val engineCopy = engine
+ return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
+ engineCopy,
+ "InteractiveWatchFaceImpl.getComplicationDetails",
+ WatchFaceService.Companion.ExecutionThread.UI
+ ) { it.complicationSlotsManager.getComplicationsState(engineCopy!!.screenBounds) }
}
override fun getUserStyleSchema(): UserStyleSchemaWireFormat? {
- return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnBinderThread(
+ return WatchFaceService.awaitDeferredEarlyInitDetailsThenRunOnThread(
engine,
- "InteractiveWatchFaceImpl.getUserStyleSchema"
+ "InteractiveWatchFaceImpl.getUserStyleSchema",
+ WatchFaceService.Companion.ExecutionThread.CURRENT
) { it.userStyleRepository.schema.toWireFormat() }
}
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 97ca016..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(
@@ -5801,7 +5830,7 @@
}
@Test
- public fun setPendingInitialComplications() {
+ public fun setComplicationDataListMergesCorrectly() {
initEngine(
WatchFaceType.ANALOG,
listOf(leftComplication, rightComplication),
@@ -5829,15 +5858,17 @@
.build()
)
- engineWrapper.setPendingInitialComplications(listOf(left1))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1)
+ engineWrapper.setComplicationDataList(listOf(left1))
+ // In initEngine we fill initial complication data using
+ // setComplicationViaWallpaperCommand, that's why lastComplications initially is not empty
+ assertThat(engineWrapper.complicationsFlow.value).contains(left1)
// Check merges are working as expected.
- engineWrapper.setPendingInitialComplications(listOf(right))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left1, right)
+ engineWrapper.setComplicationDataList(listOf(right))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left1, right)
- engineWrapper.setPendingInitialComplications(listOf(left2))
- assertThat(engineWrapper.pendingInitialComplications).containsExactly(left2, right)
+ engineWrapper.setComplicationDataList(listOf(left2))
+ assertThat(engineWrapper.complicationsFlow.value).containsExactly(left2, right)
}
@Test
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.