Merge "[FocusAndMetering] Document exceptions for startFocusAndMetering to align with other CameraControl APIs" into androidx-main
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/TestApkSha256ReportTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/TestApkSha256ReportTest.kt
deleted file mode 100644
index b513ba9..0000000
--- a/buildSrc-tests/src/test/kotlin/androidx/build/testConfiguration/TestApkSha256ReportTest.kt
+++ /dev/null
@@ -1,83 +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.build.testConfiguration
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.rules.TemporaryFolder
-
-class TestApkSha256ReportTest {
- @get:Rule
- val temporaryFolder = TemporaryFolder()
-
- @Test
- fun writeShaXml() {
- val file1 = temporaryFolder.newFile(
- "foo.txt"
- ).also {
- it.writeText(
- "test-file-1"
- )
- }
- // validated via an external tool so we don't rely on what we generate.
- val file1Sha = "db7bb0ef3ae21cafba57068bab4bcdd5129ba8a25ef5f8c16ad33fc686c7467e"
- val output = temporaryFolder.newFile("output.xml")
- val subject = TestApkSha256Report()
- subject.addFile("renamed.txt", file1)
- subject.writeToFile(output)
- assertThat(
- output.readText(Charsets.UTF_8).trimIndent()
- ).isEqualTo(
- """
- <?xml version="1.0" encoding="UTF-8" standalone="no"?>
- <sha256Report>
- <file name="renamed.txt" sha256="$file1Sha"/>
- </sha256Report>
- """.trimIndent()
- )
- }
-
- @Test
- fun conflictingFileName() {
- val file1 = temporaryFolder.newFile(
- "foo.txt"
- ).also {
- it.writeText(
- "test-file-1"
- )
- }
- val file2 = temporaryFolder.newFile(
- "foo2.txt"
- ).also {
- it.writeText(
- "test-file-2"
- )
- }
- val subject = TestApkSha256Report()
- subject.addFile("file1.txt", file1)
- val result = runCatching {
- // intentionally use the same name for a different file
- subject.addFile("file1.txt", file2)
- }
- assertThat(
- result.exceptionOrNull()
- ).hasMessageThat().contains(
- "Same file name sent with different sha256 values"
- )
- }
-}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
index 4e9b6b1e..49a1ff3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
@@ -128,6 +128,9 @@
"generateReleaseProtos",
// Release APKs
"copyReleaseApk",
+ // The following tests intentionally have the same output of golden images
+ "updateGoldenDesktopTest",
+ "updateGoldenDebugUnitTest"
)
val taskTypesKnownToDuplicateOutputs = setOf(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
index 89e34eb..8137fdd 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
@@ -50,7 +50,11 @@
val paparazziNative = project.createUnzippedPaparazziNativeDependency()
project.afterEvaluate { it.addTestUtilsDependency() }
project.tasks.register("updateGolden")
- project.tasks.withType<Test>().configureEach { it.configureTestTask(paparazziNative) }
+ project.afterEvaluate {
+ // need to be inside of afterEvaluate because we read android.namespace
+ // ideally, we refactor to use a lazy API
+ project.tasks.withType<Test>().configureEach { it.configureTestTask(paparazziNative) }
+ }
project.tasks.withType<Test>().whenTaskAdded { project.registerUpdateGoldenTask(it) }
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
index f7bcd5c..f9ce277 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/AndroidTestConfigBuilder.kt
@@ -98,17 +98,7 @@
.append(APK_INSTALL_OPTION.replace("APK_NAME", testApkName))
if (!appApkName.isNullOrEmpty())
sb.append(APK_INSTALL_OPTION.replace("APK_NAME", appApkName!!))
- // Temporary hardcoded hack for b/181810492
- else if (applicationId == "androidx.benchmark.macro.test") {
- sb.append(
- APK_INSTALL_OPTION.replace(
- "APK_NAME",
- /* ktlint-disable max-line-length */
- "benchmark-integration-tests-macrobenchmark-target_macrobenchmark-target-release.apk"
- /* ktlint-enable max-line-length */
- )
- )
- }
+
sb.append(TARGET_PREPARER_CLOSE)
.append(TEST_BLOCK_OPEN)
.append(RUNNER_OPTION.replace("TEST_RUNNER", testRunner))
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
index b9861d0..a1b8cb3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/GenerateTestConfigurationTask.kt
@@ -19,9 +19,11 @@
import androidx.build.dependencyTracker.ProjectSubset
import androidx.build.renameApkForTesting
import com.android.build.api.variant.BuiltArtifactsLoader
+import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
@@ -32,8 +34,6 @@
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.TaskAction
-import java.io.File
-import org.gradle.api.provider.ListProperty
/**
* Writes a configuration file in
@@ -94,41 +94,23 @@
@get:OutputFile
abstract val outputJson: RegularFileProperty
- /**
- * Output file where we write the sha256 for each APK file we reference
- */
- @get:OutputFile
- abstract val shaReportOutput: RegularFileProperty
-
@get:OutputFile
abstract val constrainedOutputXml: RegularFileProperty
- /**
- * Output file where we write the sha256 for each APK file we reference in constrained setup
- */
- @get:OutputFile
- abstract val constrainedShaReportOutput: RegularFileProperty
-
@TaskAction
fun generateAndroidTestZip() {
- val testApkSha256Report = TestApkSha256Report()
writeConfigFileContent(
outputFile = constrainedOutputXml,
- testApkSha256Report = testApkSha256Report,
isConstrained = true,
)
writeConfigFileContent(
outputFile = outputXml,
- testApkSha256Report = testApkSha256Report,
isConstrained = false,
)
- testApkSha256Report.writeToFile(shaReportOutput.get().asFile)
- testApkSha256Report.writeToFile(constrainedShaReportOutput.get().asFile)
}
private fun writeConfigFileContent(
outputFile: RegularFileProperty,
- testApkSha256Report: TestApkSha256Report,
isConstrained: Boolean = false
) {
/*
@@ -148,7 +130,6 @@
.renameApkForTesting(appProjectPath.get(), hasBenchmarkPlugin = false)
configBuilder.appApkName(appName)
.appApkSha256(sha256(File(appApkBuiltArtifact.outputFile)))
- testApkSha256Report.addFile(appName, appApkBuiltArtifact)
}
configBuilder.additionalApkKeys(additionalApkKeys.get())
val isPresubmit = presubmit.get()
@@ -216,7 +197,6 @@
.minSdk(minSdk.get().toString())
.testRunner(testRunner.get())
.testApkSha256(sha256(File(testApkBuiltArtifact.outputFile)))
- testApkSha256Report.addFile(testName, testApkBuiltArtifact)
createOrFail(outputFile).writeText(configBuilder.buildXml())
if (!isConstrained) {
createOrFail(outputJson).writeText(configBuilder.buildJson())
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestApkSha256Report.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestApkSha256Report.kt
index 0c971b8..7fd7d05 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestApkSha256Report.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestApkSha256Report.kt
@@ -16,80 +16,9 @@
package androidx.build.testConfiguration
-import com.android.build.api.variant.BuiltArtifact
-import com.google.common.annotations.VisibleForTesting
import com.google.common.hash.Hashing
import com.google.common.io.BaseEncoding
import java.io.File
-import java.io.FileOutputStream
-import javax.xml.parsers.DocumentBuilderFactory
-import javax.xml.transform.OutputKeys
-import javax.xml.transform.TransformerFactory
-import javax.xml.transform.dom.DOMSource
-import javax.xml.transform.stream.StreamResult
-
-/**
- * Helper class to record sha256 of APK files referenced in tests.
- *
- * It hashes the files the same way androidx-ci-action does to utilize the APK cache in Google Cloud
- * Storage (GCP). This hashing helps us avoid uploading the same APK multiple times to GCP.
- *
- * https://github.com/androidx/androidx-ci-action/blob/main/AndroidXCI/lib/src/main/kotlin/dev/androidx/ci/util/HashUtil.kt#L21
- */
-@VisibleForTesting
-class TestApkSha256Report {
- private val files = mutableMapOf<String, String>()
-
- /**
- * Adds the given builtArtifact to the list of shas after calculating its sha256 hash.
- */
- fun addFile(name: String, builtArtifact: BuiltArtifact) {
- addFile(name, File(builtArtifact.outputFile))
- }
-
- fun addFile(name: String, file: File) {
- require(file.exists()) {
- "Cannot find file ${file.path}"
- }
- val hash = sha256(file)
- val existing = files[name]
- require(existing == null || existing == hash) {
- "Same file name sent with different sha256 values. $name"
- }
- files[name] = hash
- }
-
- /**
- * Writes the [TestApkSha256Report] in XML format into the given [file].
- */
- fun writeToFile(file: File) {
- if (file.exists()) {
- file.delete()
- }
- file.parentFile.mkdirs()
- val factory = DocumentBuilderFactory.newInstance()
- val builder = factory.newDocumentBuilder()
- val doc = builder.newDocument()
- val root = doc.createElement("sha256Report")
- doc.appendChild(root)
- files.entries.sortedBy {
- it.key
- }.forEach { (fileName, hash) ->
- val elm = doc.createElement("file")
- elm.setAttribute("name", fileName)
- elm.setAttribute("sha256", hash)
- root.appendChild(elm)
- }
- val transformerFactory = TransformerFactory.newInstance()
- val transformer = transformerFactory.newTransformer()
- transformer.setOutputProperty(OutputKeys.INDENT, "yes")
- val source = DOMSource(doc)
- FileOutputStream(file).use { fileOutput ->
- val result = StreamResult(fileOutput)
- transformer.transform(source, result)
- }
- }
-}
@Suppress("UnstableApiUsage") // guava Hashing is marked as @Beta
internal fun sha256(file: File): String {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
index 37e6689..3bc7114 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/testConfiguration/TestSuiteConfiguration.kt
@@ -63,7 +63,6 @@
) {
val xmlName = "${path.asFilenamePrefix()}$variantName.xml"
val jsonName = "${path.asFilenamePrefix()}$variantName.json"
- val sha256XmlName = "${path.asFilenamePrefix()}$variantName$SHA_256_FILE_SUFFIX"
rootProject.tasks.named("createModuleInfo").configure {
it as ModuleInfoGenerator
it.testModules.add(
@@ -84,11 +83,7 @@
task.additionalApkKeys.set(androidXExtension.additionalDeviceTestApkKeys)
task.outputXml.fileValue(File(getTestConfigDirectory(), xmlName))
task.outputJson.fileValue(File(getTestConfigDirectory(), jsonName))
- task.shaReportOutput.fileValue(File(getTestConfigDirectory(), sha256XmlName))
task.constrainedOutputXml.fileValue(File(getConstrainedTestConfigDirectory(), xmlName))
- task.constrainedShaReportOutput.fileValue(
- File(getConstrainedTestConfigDirectory(), sha256XmlName)
- )
task.presubmit.set(isPresubmitBuild())
// Disable work tests on < API 18: b/178127496
if (path.startsWith(":work:")) {
@@ -135,6 +130,25 @@
*/
fun Project.addAppApkToTestConfigGeneration(overrideProject: Project = this) {
if (project.isMacrobenchmarkTarget()) {
+ if (path == ":benchmark:integration-tests:macrobenchmark-target") {
+ // Really ugly workaround for b/178776319 and b/181810492 where we hardcode that
+ // :benchmark:integration-tests:macrobenchmark-target needs to be installed
+ // for :benchmark:benchmark-macro tests to work.
+ extensions.getByType<ApplicationAndroidComponentsExtension>().apply {
+ onVariants(selector().withBuildType("release")) { appVariant ->
+ project(":benchmark:benchmark-macro").tasks.withType(
+ GenerateTestConfigurationTask::class.java
+ ).named(
+ "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}debugAndroidTest"
+ ).configure { configTask ->
+ configTask as GenerateTestConfigurationTask
+ configTask.appFolder.set(appVariant.artifacts.get(SingleArtifact.APK))
+ configTask.appLoader.set(appVariant.artifacts.getBuiltArtifactsLoader())
+ configTask.appProjectPath.set(overrideProject.path)
+ }
+ }
+ }
+ }
return
}
@@ -359,20 +373,6 @@
testRunner: String
) {
val configTask = getOrCreateMacrobenchmarkConfigTask(variantName)
- if (path == ":benchmark:integration-tests:macrobenchmark-target" && variantName == "release") {
- // Really ugly workaround for b/178776319 where we hardcode that
- // :benchmark:integration-tests:macrobenchmark-target needs to be installed
- // for :benchmark:benchmark-macro tests to work.
- project(":benchmark:benchmark-macro").tasks.withType(
- GenerateTestConfigurationTask::class.java
- ).named(
- "${AndroidXImplPlugin.GENERATE_TEST_CONFIGURATION_TASK}$variantName"
- ).configure { task ->
- task.appFolder.set(artifacts.get(SingleArtifact.APK))
- task.appLoader.set(artifacts.getBuiltArtifactsLoader())
- task.appProjectPath.set(path)
- }
- }
if (path.endsWith("macrobenchmark")) {
configTask.configure { task ->
val androidXExtension = extensions.getByType<AndroidXExtension>()
@@ -386,24 +386,12 @@
task.outputJson.fileValue(
File(getTestConfigDirectory(), "$fileNamePrefix.json")
)
- task.shaReportOutput.fileValue(
- File(
- this.getTestConfigDirectory(),
- "${this.path.asFilenamePrefix()}$variantName$SHA_256_FILE_SUFFIX"
- )
- )
task.constrainedOutputXml.fileValue(
File(
this.getTestConfigDirectory(),
"${this.path.asFilenamePrefix()}$variantName.xml"
)
)
- task.constrainedShaReportOutput.fileValue(
- File(
- this.getTestConfigDirectory(),
- "${this.path.asFilenamePrefix()}$variantName$SHA_256_FILE_SUFFIX"
- )
- )
task.minSdk.set(minSdk)
task.hasBenchmarkPlugin.set(this.hasBenchmarkPlugin())
task.testRunner.set(testRunner)
@@ -503,9 +491,3 @@
}
}
}
-
-/**
- * Suffix to add for xml files which include the SHA256 of referred APKs.
- * see: [TestApkSha256Report].
- */
-private const val SHA_256_FILE_SUFFIX = "-sha256Report.xml"
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
new file mode 100644
index 0000000..c61aa16
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EncoderProfilesProviderAdapterDeviceTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 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.pipe.integration
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.os.Build
+import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class EncoderProfilesProviderAdapterDeviceTest(private val quality: Int) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Array<Array<Int>> = arrayOf(
+ arrayOf(CamcorderProfile.QUALITY_LOW),
+ arrayOf(CamcorderProfile.QUALITY_HIGH),
+ arrayOf(CamcorderProfile.QUALITY_QCIF),
+ arrayOf(CamcorderProfile.QUALITY_CIF),
+ arrayOf(CamcorderProfile.QUALITY_480P),
+ arrayOf(CamcorderProfile.QUALITY_720P),
+ arrayOf(CamcorderProfile.QUALITY_1080P),
+ arrayOf(CamcorderProfile.QUALITY_QVGA),
+ arrayOf(CamcorderProfile.QUALITY_2160P),
+ arrayOf(CamcorderProfile.QUALITY_VGA),
+ arrayOf(CamcorderProfile.QUALITY_4KDCI),
+ arrayOf(CamcorderProfile.QUALITY_QHD),
+ arrayOf(CamcorderProfile.QUALITY_2K)
+ )
+ }
+
+ private lateinit var encoderProfilesProvider: EncoderProfilesProviderAdapter
+ private var cameraId = ""
+ private var intCameraId = -1
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+ @Before
+ fun setup() {
+ skipTestOnProblematicBuildsOfCuttlefishApi33()
+ Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+ cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+ intCameraId = cameraId.toInt()
+
+ encoderProfilesProvider = EncoderProfilesProviderAdapter(cameraId)
+ }
+
+ @Test
+ fun hasProfile_returnSameResult() {
+ assertThat(encoderProfilesProvider.hasProfile(quality))
+ .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+ }
+
+ @Test
+ fun hasProfile_getReturnNonNull() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+ }
+
+ @Test
+ fun notHasProfile_getReturnNull() {
+ Assume.assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun hasSameContentAsCamcorderProfile() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profile = CamcorderProfile.get(quality)
+ val encoderProfiles = encoderProfilesProvider.getAll(quality)
+ val videoProfile = encoderProfiles!!.videoProfiles[0]
+ val audioProfile = encoderProfiles.audioProfiles[0]
+
+ assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+ assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+ assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+ assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+ assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+ assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+ assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+ assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+ assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+ assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+ assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+ }
+
+ @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+ @Test
+ fun api31Api32_hasSameContentAsEncoderProfiles() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+ assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ fun afterApi33_hasSameContentAsEncoderProfiles() {
+ Assume.assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+ assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ // TODO: removes after b/265613005 is fixed
+ private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+ // Skip test for b/265613005
+ Assume.assumeFalse(
+ "Cuttlefish has null VideoProfile issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+ Build.ID.startsWith("TP1A")
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
new file mode 100644
index 0000000..224eeab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EncoderProfilesProviderAdapter.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 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.pipe.integration.adapter
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.Nullable
+import androidx.annotation.RequiresApi
+import androidx.camera.core.Logger
+import androidx.camera.core.impl.EncoderProfilesProvider
+import androidx.camera.core.impl.EncoderProfilesProxy
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat
+
+/**
+ * Adapt the [EncoderProfilesProvider] interface to [CameraPipe].
+ */
+@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+class EncoderProfilesProviderAdapter(private val cameraIdString: String) : EncoderProfilesProvider {
+ private val hasValidCameraId: Boolean
+ private val cameraId: Int
+
+ init {
+ var hasValidCameraId = false
+ var intCameraId = -1
+ try {
+ intCameraId = cameraIdString.toInt()
+ hasValidCameraId = true
+ } catch (e: NumberFormatException) {
+ Logger.w(
+ TAG, "Camera id is not an integer: $cameraIdString, unable to create" +
+ " EncoderProfilesProviderAdapter."
+ )
+ }
+ this.hasValidCameraId = hasValidCameraId
+ cameraId = intCameraId
+
+ // TODO(b/241296464): CamcorderProfileResolutionQuirk
+ }
+
+ override fun hasProfile(quality: Int): Boolean {
+ if (!hasValidCameraId) {
+ return false
+ }
+ return CamcorderProfile.hasProfile(cameraId, quality)
+ }
+
+ override fun getAll(quality: Int): EncoderProfilesProxy? {
+ if (!hasValidCameraId) {
+ return null
+ }
+ if (!CamcorderProfile.hasProfile(cameraId, quality)) {
+ return null
+ }
+ return getProfilesInternal(quality)
+ }
+
+ @Nullable
+ @Suppress("DEPRECATION")
+ private fun getProfilesInternal(quality: Int): EncoderProfilesProxy? {
+ return if (Build.VERSION.SDK_INT >= 31) {
+ val profiles: EncoderProfiles? = Api31Impl.getAll(cameraIdString, quality)
+ if (profiles != null) EncoderProfilesProxyCompat.from(profiles) else null
+ } else {
+ var profile: CamcorderProfile? = null
+ try {
+ profile = CamcorderProfile.get(cameraId, quality)
+ } catch (e: RuntimeException) {
+ // CamcorderProfile.get() will throw
+ // - RuntimeException if not able to retrieve camcorder profile params.
+ // - IllegalArgumentException if quality is not valid.
+ Logger.w(TAG, "Unable to get CamcorderProfile by quality: $quality", e)
+ }
+ if (profile != null) EncoderProfilesProxyCompat.from(profile) else null
+ }
+ }
+
+ @RequiresApi(31)
+ internal object Api31Impl {
+ @DoNotInline
+ fun getAll(cameraId: String, quality: Int): EncoderProfiles? {
+ return CamcorderProfile.getAll(cameraId, quality)
+ }
+ }
+
+ companion object {
+ private const val TAG = "EncoderProfilesProviderAdapter"
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..8a87d6a
--- /dev/null
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProviderTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 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
+
+import android.media.CamcorderProfile
+import android.media.EncoderProfiles.VideoProfile.YUV_420
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.os.Build
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.CameraUtil
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@RunWith(Parameterized::class)
+@SmallTest
+@SdkSuppress(minSdkVersion = 21)
+class Camera2EncoderProfilesProviderTest(private val quality: Int) {
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters
+ fun data(): Array<Array<Int>> = arrayOf(
+ arrayOf(CamcorderProfile.QUALITY_LOW),
+ arrayOf(CamcorderProfile.QUALITY_HIGH),
+ arrayOf(CamcorderProfile.QUALITY_QCIF),
+ arrayOf(CamcorderProfile.QUALITY_CIF),
+ arrayOf(CamcorderProfile.QUALITY_480P),
+ arrayOf(CamcorderProfile.QUALITY_720P),
+ arrayOf(CamcorderProfile.QUALITY_1080P),
+ arrayOf(CamcorderProfile.QUALITY_QVGA),
+ arrayOf(CamcorderProfile.QUALITY_2160P),
+ arrayOf(CamcorderProfile.QUALITY_VGA),
+ arrayOf(CamcorderProfile.QUALITY_4KDCI),
+ arrayOf(CamcorderProfile.QUALITY_QHD),
+ arrayOf(CamcorderProfile.QUALITY_2K)
+ )
+ }
+
+ private lateinit var encoderProfilesProvider: Camera2EncoderProfilesProvider
+ private var cameraId = ""
+ private var intCameraId = -1
+
+ @get:Rule
+ val useCamera = CameraUtil.grantCameraPermissionAndPreTest()
+
+ @Before
+ fun setup() {
+ skipTestOnProblematicBuildsOfCuttlefishApi33()
+ assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK))
+
+ cameraId = CameraUtil.getCameraIdWithLensFacing(CameraSelector.LENS_FACING_BACK)!!
+ intCameraId = cameraId.toInt()
+
+ encoderProfilesProvider = Camera2EncoderProfilesProvider(cameraId)
+ }
+
+ @Test
+ fun hasProfile_returnSameResult() {
+ assertThat(encoderProfilesProvider.hasProfile(quality))
+ .isEqualTo(CamcorderProfile.hasProfile(intCameraId, quality))
+ }
+
+ @Test
+ fun hasProfile_getReturnNonNull() {
+ assumeTrue(CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNotNull()
+ }
+
+ @Test
+ fun notHasProfile_getReturnNull() {
+ assumeTrue(!CamcorderProfile.hasProfile(intCameraId, quality))
+
+ assertThat(encoderProfilesProvider.getAll(quality)).isNull()
+ }
+
+ @Suppress("DEPRECATION")
+ @Test
+ fun hasSameContentAsCamcorderProfile() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profile = CamcorderProfile.get(quality)
+ val encoderProfiles = encoderProfilesProvider.getAll(quality)
+ val videoProfile = encoderProfiles!!.videoProfiles[0]
+ val audioProfile = encoderProfiles.audioProfiles[0]
+
+ assertThat(encoderProfiles.defaultDurationSeconds).isEqualTo(profile.duration)
+ assertThat(encoderProfiles.recommendedFileFormat).isEqualTo(profile.fileFormat)
+ assertThat(videoProfile.codec).isEqualTo(profile.videoCodec)
+ assertThat(videoProfile.bitrate).isEqualTo(profile.videoBitRate)
+ assertThat(videoProfile.frameRate).isEqualTo(profile.videoFrameRate)
+ assertThat(videoProfile.width).isEqualTo(profile.videoFrameWidth)
+ assertThat(videoProfile.height).isEqualTo(profile.videoFrameHeight)
+ assertThat(audioProfile.codec).isEqualTo(profile.audioCodec)
+ assertThat(audioProfile.bitrate).isEqualTo(profile.audioBitRate)
+ assertThat(audioProfile.sampleRate).isEqualTo(profile.audioSampleRate)
+ assertThat(audioProfile.channels).isEqualTo(profile.audioChannels)
+ }
+
+ @SdkSuppress(minSdkVersion = 31, maxSdkVersion = 32)
+ @Test
+ fun api31Api32_hasSameContentAsEncoderProfiles() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(BIT_DEPTH_8)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(YUV_420)
+ assertThat(videoProxy.hdrFormat).isEqualTo(HDR_NONE)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ @SdkSuppress(minSdkVersion = 33)
+ @Test
+ fun afterApi33_hasSameContentAsEncoderProfiles() {
+ assumeTrue(CamcorderProfile.hasProfile(quality))
+
+ val profiles = CamcorderProfile.getAll(cameraId, quality)
+ val video = profiles!!.videoProfiles[0]
+ val audio = profiles.audioProfiles[0]
+ val profilesProxy = encoderProfilesProvider.getAll(quality)
+ val videoProxy = profilesProxy!!.videoProfiles[0]
+ val audioProxy = profilesProxy.audioProfiles[0]
+
+ assertThat(profilesProxy.defaultDurationSeconds).isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(profilesProxy.recommendedFileFormat).isEqualTo(profiles.recommendedFileFormat)
+ assertThat(videoProxy.codec).isEqualTo(video.codec)
+ assertThat(videoProxy.mediaType).isEqualTo(video.mediaType)
+ assertThat(videoProxy.bitrate).isEqualTo(video.bitrate)
+ assertThat(videoProxy.frameRate).isEqualTo(video.frameRate)
+ assertThat(videoProxy.width).isEqualTo(video.width)
+ assertThat(videoProxy.height).isEqualTo(video.height)
+ assertThat(videoProxy.profile).isEqualTo(video.profile)
+ assertThat(videoProxy.bitDepth).isEqualTo(video.bitDepth)
+ assertThat(videoProxy.chromaSubsampling).isEqualTo(video.chromaSubsampling)
+ assertThat(videoProxy.hdrFormat).isEqualTo(video.hdrFormat)
+ assertThat(audioProxy.codec).isEqualTo(audio.codec)
+ assertThat(audioProxy.mediaType).isEqualTo(audio.mediaType)
+ assertThat(audioProxy.bitrate).isEqualTo(audio.bitrate)
+ assertThat(audioProxy.sampleRate).isEqualTo(audio.sampleRate)
+ assertThat(audioProxy.channels).isEqualTo(audio.channels)
+ assertThat(audioProxy.profile).isEqualTo(audio.profile)
+ }
+
+ // TODO: removes after b/265613005 is fixed
+ private fun skipTestOnProblematicBuildsOfCuttlefishApi33() {
+ // Skip test for b/265613005
+ Assume.assumeFalse(
+ "Cuttlefish has null VideoProfile issue. Unable to test.",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 33 &&
+ Build.ID.startsWith("TP1A")
+ )
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
new file mode 100644
index 0000000..a35596c
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2EncoderProfilesProvider.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2023 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;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.compat.EncoderProfilesProxyCompat;
+
+/** An implementation that provides the {@link EncoderProfilesProxy}. */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class Camera2EncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private static final String TAG = "Camera2EncoderProfilesProvider";
+
+ private final boolean mHasValidCameraId;
+ private final String mCameraId;
+ private final int mIntCameraId;
+
+ public Camera2EncoderProfilesProvider(@NonNull String cameraId) {
+ mCameraId = cameraId;
+ boolean hasValidCameraId = false;
+ int intCameraId = -1;
+ try {
+ intCameraId = Integer.parseInt(cameraId);
+ hasValidCameraId = true;
+ } catch (NumberFormatException e) {
+ Logger.w(TAG, "Camera id is not an integer: " + cameraId
+ + ", unable to create Camera2EncoderProfilesProvider");
+ }
+ mHasValidCameraId = hasValidCameraId;
+ mIntCameraId = intCameraId;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ if (!mHasValidCameraId) {
+ return false;
+ }
+
+ return CamcorderProfile.hasProfile(mIntCameraId, quality);
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ if (!mHasValidCameraId) {
+ return null;
+ }
+
+ if (!CamcorderProfile.hasProfile(mIntCameraId, quality)) {
+ return null;
+ }
+
+ return getProfilesInternal(quality);
+ }
+
+ @Nullable
+ @SuppressWarnings("deprecation")
+ private EncoderProfilesProxy getProfilesInternal(int quality) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ EncoderProfiles profiles = Api31Impl.getAll(mCameraId, quality);
+ return profiles != null ? EncoderProfilesProxyCompat.from(profiles) : null;
+ } else {
+ CamcorderProfile profile = null;
+ try {
+ profile = CamcorderProfile.get(mIntCameraId, quality);
+ } catch (RuntimeException e) {
+ // CamcorderProfile.get() will throw
+ // - RuntimeException if not able to retrieve camcorder profile params.
+ // - IllegalArgumentException if quality is not valid.
+ Logger.w(TAG, "Unable to get CamcorderProfile by quality: " + quality, e);
+ }
+ return profile != null ? EncoderProfilesProxyCompat.from(profile) : null;
+ }
+ }
+
+ @RequiresApi(31)
+ static class Api31Impl {
+ @DoNotInline
+ static EncoderProfiles getAll(String cameraId, int quality) {
+ return CamcorderProfile.getAll(cameraId, quality);
+ }
+
+ // This class is not instantiable.
+ private Api31Impl() {
+ }
+ }
+}
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 fb2d013..86a0c16 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
@@ -28,7 +28,7 @@
import androidx.camera.camera2.internal.compat.workaround.CamcorderProfileResolutionValidator;
import androidx.camera.core.Logger;
import androidx.camera.core.impl.ImageFormatConstants;
-import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
import java.util.ArrayList;
import java.util.Arrays;
@@ -55,7 +55,7 @@
* @see CamcorderProfileResolutionValidator
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class CamcorderProfileResolutionQuirk implements Quirk {
+public class CamcorderProfileResolutionQuirk implements ProfileResolutionQuirk {
private static final String TAG = "CamcorderProfileResolutionQuirk";
static boolean load(@NonNull CameraCharacteristicsCompat characteristicsCompat) {
@@ -86,6 +86,7 @@
}
/** Returns the supported video resolutions. */
+ @Override
@NonNull
public List<Size> getSupportedResolutions() {
return new ArrayList<>(mSupportedResolutions);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
new file mode 100644
index 0000000..1e041d6
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2023 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.media.CamcorderProfile;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+/**
+ * EncoderProfilesProvider is used to obtain the {@link EncoderProfilesProxy}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface EncoderProfilesProvider {
+
+ /**
+ * Checks if the quality is supported on this device.
+ *
+ * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+ */
+ boolean hasProfile(int quality);
+
+ /**
+ * Gets the {@link EncoderProfilesProxy} if the quality is supported on the device.
+ *
+ * <p>The quality should be one of quality constants defined in {@link CamcorderProfile}.
+ *
+ * @see #hasProfile(int)
+ */
+ @Nullable
+ EncoderProfilesProxy getAll(int quality);
+
+ /** An implementation that contains no data. */
+ EncoderProfilesProvider EMPTY = new EncoderProfilesProvider() {
+ @Override
+ public boolean hasProfile(int quality) {
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ return null;
+ }
+ };
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
new file mode 100644
index 0000000..f37cb38
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2023 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 static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import static java.util.Collections.unmodifiableList;
+
+import android.media.EncoderProfiles;
+import android.media.MediaRecorder;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import com.google.auto.value.AutoValue;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * EncoderProfilesProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface EncoderProfilesProxy {
+
+ /** Constant representing no codec profile. */
+ int CODEC_PROFILE_NONE = -1;
+
+ /** @see EncoderProfiles#getDefaultDurationSeconds() */
+ int getDefaultDurationSeconds();
+
+ /** @see EncoderProfiles#getRecommendedFileFormat() */
+ int getRecommendedFileFormat();
+
+ /** @see EncoderProfiles#getAudioProfiles() */
+ @NonNull
+ List<AudioProfileProxy> getAudioProfiles();
+
+ /** @see EncoderProfiles#getVideoProfiles() */
+ @NonNull
+ List<VideoProfileProxy> getVideoProfiles();
+
+ /**
+ * VideoProfileProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles.VideoProfile}.
+ */
+ @AutoValue
+ abstract class VideoProfileProxy {
+
+ /** Constant representing no media type. */
+ public static final String MEDIA_TYPE_NONE = "video/none";
+
+ /** Constant representing bit depth 8. */
+ public static final int BIT_DEPTH_8 = 8;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({H263, H264, HEVC, VP8, MPEG_4_SP, VP9, DOLBY_VISION, AV1,
+ MediaRecorder.VideoEncoder.DEFAULT})
+ public @interface VideoEncoder {
+ }
+
+ /** Creates a VideoProfileProxy instance. */
+ @NonNull
+ public static VideoProfileProxy create(
+ @VideoEncoder int codec,
+ @NonNull String mediaType,
+ int bitrate,
+ int frameRate,
+ int width,
+ int height,
+ int profile,
+ int bitDepth,
+ int chromaSubsampling,
+ int hdrFormat) {
+ return new AutoValue_EncoderProfilesProxy_VideoProfileProxy(
+ codec,
+ mediaType,
+ bitrate,
+ frameRate,
+ width,
+ height,
+ profile,
+ bitDepth,
+ chromaSubsampling,
+ hdrFormat
+ );
+ }
+
+ /** @see EncoderProfiles.VideoProfile#getCodec() */
+ @VideoEncoder
+ public abstract int getCodec();
+
+ /** @see EncoderProfiles.VideoProfile#getMediaType() */
+ @NonNull
+ public abstract String getMediaType();
+
+ /** @see EncoderProfiles.VideoProfile#getBitrate() */
+ public abstract int getBitrate();
+
+ /** @see EncoderProfiles.VideoProfile#getFrameRate() */
+ public abstract int getFrameRate();
+
+ /** @see EncoderProfiles.VideoProfile#getWidth() */
+ public abstract int getWidth();
+
+ /** @see EncoderProfiles.VideoProfile#getHeight() */
+ public abstract int getHeight();
+
+ /** @see EncoderProfiles.VideoProfile#getProfile() */
+ public abstract int getProfile();
+
+ /** @see EncoderProfiles.VideoProfile#getBitDepth() */
+ public abstract int getBitDepth();
+
+ /** @see EncoderProfiles.VideoProfile#getChromaSubsampling() */
+ public abstract int getChromaSubsampling();
+
+ /** @see EncoderProfiles.VideoProfile#getHdrFormat() */
+ public abstract int getHdrFormat();
+ }
+
+ /**
+ * AudioProfileProxy defines the get methods that is mapping to the fields of
+ * {@link EncoderProfiles.AudioProfile}.
+ */
+ @AutoValue
+ abstract class AudioProfileProxy {
+
+ /** Constant representing no media type. */
+ public static final String MEDIA_TYPE_NONE = "audio/none";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({AAC, AAC_ELD, AMR_NB, AMR_WB, HE_AAC, OPUS, VORBIS,
+ MediaRecorder.AudioEncoder.DEFAULT})
+ public @interface AudioEncoder {
+ }
+
+ /** Creates an AudioProfileProxy instance. */
+ @NonNull
+ public static AudioProfileProxy create(
+ @AudioEncoder int codec,
+ @NonNull String mediaType,
+ int bitRate,
+ int sampleRate,
+ int channels,
+ int profile) {
+ return new AutoValue_EncoderProfilesProxy_AudioProfileProxy(
+ codec,
+ mediaType,
+ bitRate,
+ sampleRate,
+ channels,
+ profile
+ );
+ }
+
+ /** @see EncoderProfiles.AudioProfile#getCodec() */
+ @AudioEncoder
+ public abstract int getCodec();
+
+ /** @see EncoderProfiles.AudioProfile#getMediaType() */
+ @NonNull
+ public abstract String getMediaType();
+
+ /** @see EncoderProfiles.AudioProfile#getBitrate() */
+ public abstract int getBitrate();
+
+ /** @see EncoderProfiles.AudioProfile#getSampleRate() */
+ public abstract int getSampleRate();
+
+ /** @see EncoderProfiles.AudioProfile#getChannels() */
+ public abstract int getChannels();
+
+ /** @see EncoderProfiles.AudioProfile#getProfile() */
+ public abstract int getProfile();
+ }
+
+ /**
+ * An implementation of {@link EncoderProfilesProxy} that is immutable.
+ */
+ @AutoValue
+ abstract class ImmutableEncoderProfilesProxy implements EncoderProfilesProxy {
+
+ /** Creates an EncoderProfilesProxy instance. */
+ @NonNull
+ public static ImmutableEncoderProfilesProxy create(
+ int defaultDurationSeconds,
+ int recommendedFileFormat,
+ @NonNull List<AudioProfileProxy> audioProfiles,
+ @NonNull List<VideoProfileProxy> videoProfiles) {
+ return new AutoValue_EncoderProfilesProxy_ImmutableEncoderProfilesProxy(
+ defaultDurationSeconds,
+ recommendedFileFormat,
+ unmodifiableList(new ArrayList<>(audioProfiles)),
+ unmodifiableList(new ArrayList<>(videoProfiles))
+ );
+ }
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
new file mode 100644
index 0000000..1c54463
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesResolutionValidator.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023 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.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Validates the video resolution of {@link EncoderProfiles}.
+ *
+ * @see ProfileResolutionQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class EncoderProfilesResolutionValidator {
+
+ @NonNull
+ private final List<ProfileResolutionQuirk> mQuirks;
+ @NonNull
+ private final Set<Size> mSupportedResolutions;
+
+ public EncoderProfilesResolutionValidator(@Nullable List<ProfileResolutionQuirk> quirks) {
+ mQuirks = new ArrayList<>();
+ if (quirks != null) {
+ mQuirks.addAll(quirks);
+ }
+
+ mSupportedResolutions = generateSupportedResolutions(quirks);
+ }
+
+ @NonNull
+ private Set<Size> generateSupportedResolutions(@Nullable List<ProfileResolutionQuirk> quirks) {
+ if (quirks == null || quirks.isEmpty()) {
+ return Collections.emptySet();
+ }
+
+ Set<Size> supportedResolutions = new HashSet<>(quirks.get(0).getSupportedResolutions());
+ for (int i = 1; i < quirks.size(); i++) {
+ supportedResolutions.retainAll(quirks.get(i).getSupportedResolutions());
+ }
+
+ return supportedResolutions;
+ }
+
+ /** Checks if this validator contains quirk. */
+ public boolean hasQuirk() {
+ return !mQuirks.isEmpty();
+ }
+
+ /** Checks if any video resolution of EncoderProfiles is valid. */
+ public boolean hasValidVideoResolution(@Nullable EncoderProfilesProxy profiles) {
+ if (profiles == null) {
+ return false;
+ }
+
+ if (!hasQuirk()) {
+ return !profiles.getVideoProfiles().isEmpty();
+ }
+
+ boolean hasValidResolution = false;
+ for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+ Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+ if (mSupportedResolutions.contains(videoSize)) {
+ hasValidResolution = true;
+ break;
+ }
+ }
+
+ return hasValidResolution;
+ }
+
+ /** Returns an {@link EncoderProfilesProxy} that filters out invalid resolutions. */
+ @Nullable
+ public EncoderProfilesProxy filterInvalidVideoResolution(
+ @Nullable EncoderProfilesProxy profiles) {
+ if (profiles == null) {
+ return null;
+ }
+
+ if (!hasQuirk()) {
+ return profiles;
+ }
+
+ List<VideoProfileProxy> validVideoProfiles = new ArrayList<>();
+ for (VideoProfileProxy videoProfile : profiles.getVideoProfiles()) {
+ Size videoSize = new Size(videoProfile.getWidth(), videoProfile.getHeight());
+ if (mSupportedResolutions.contains(videoSize)) {
+ validVideoProfiles.add(videoProfile);
+ }
+ }
+
+ return validVideoProfiles.isEmpty() ? null : ImmutableEncoderProfilesProxy.create(
+ profiles.getDefaultDurationSeconds(),
+ profiles.getRecommendedFileFormat(),
+ profiles.getAudioProfiles(),
+ validVideoProfiles
+ );
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
new file mode 100644
index 0000000..df32c6d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk;
+
+import java.util.List;
+
+/**
+ * An implementation that provides the {@link EncoderProfilesProxy} whose video resolutions are
+ * validated.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ResolutionValidatedEncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private final EncoderProfilesProvider mProvider;
+ private final EncoderProfilesResolutionValidator mEncoderProfilesResolutionValidator;
+
+ public ResolutionValidatedEncoderProfilesProvider(@NonNull EncoderProfilesProvider provider,
+ @NonNull Quirks quirks) {
+ mProvider = provider;
+ List<ProfileResolutionQuirk> resolutionQuirks = quirks.getAll(ProfileResolutionQuirk.class);
+ mEncoderProfilesResolutionValidator = new EncoderProfilesResolutionValidator(
+ resolutionQuirks);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ if (!mProvider.hasProfile(quality)) {
+ return false;
+ }
+
+ if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+ EncoderProfilesProxy profiles = mProvider.getAll(quality);
+ return mEncoderProfilesResolutionValidator.hasValidVideoResolution(profiles);
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ if (!mProvider.hasProfile(quality)) {
+ return null;
+ }
+
+ EncoderProfilesProxy profiles = mProvider.getAll(quality);
+ if (mEncoderProfilesResolutionValidator.hasQuirk()) {
+ profiles = mEncoderProfilesResolutionValidator.filterInvalidVideoResolution(profiles);
+ }
+
+ return profiles;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
new file mode 100644
index 0000000..cd50106
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompat.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+/**
+ * Helper for accessing features of {@link EncoderProfiles} and {@link CamcorderProfile} in a
+ * backwards compatible fashion.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesProxyCompat {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @RequiresApi(31)
+ @NonNull
+ public static EncoderProfilesProxy from(@NonNull EncoderProfiles encoderProfiles) {
+ if (Build.VERSION.SDK_INT >= 33) {
+ return EncoderProfilesProxyCompatApi33Impl.from(encoderProfiles);
+ } else if (Build.VERSION.SDK_INT >= 31) {
+ return EncoderProfilesProxyCompatApi31Impl.from(encoderProfiles);
+ } else {
+ throw new RuntimeException(
+ "Unable to call from(EncoderProfiles) on API " + Build.VERSION.SDK_INT
+ + ". Version 31 or higher required.");
+ }
+ }
+
+ /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+ @NonNull
+ public static EncoderProfilesProxy from(@NonNull CamcorderProfile camcorderProfile) {
+ if (Build.VERSION.SDK_INT >= 31) {
+ throw new RuntimeException(
+ "Should not use from(CamcorderProfile) on API " + Build.VERSION.SDK_INT
+ + ". CamcorderProfile is deprecated on API 31, use "
+ + "from(EncoderProfiles) instead.");
+ } else {
+ return EncoderProfilesProxyCompatBaseImpl.from(camcorderProfile);
+ }
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompat() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
new file mode 100644
index 0000000..57a5476
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi31Impl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(31)
+class EncoderProfilesProxyCompatApi31Impl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull EncoderProfiles encoderProfiles) {
+ return ImmutableEncoderProfilesProxy.create(
+ encoderProfiles.getDefaultDurationSeconds(),
+ encoderProfiles.getRecommendedFileFormat(),
+ fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+ fromVideoProfiles(encoderProfiles.getVideoProfiles())
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> fromVideoProfiles(
+ @NonNull List<VideoProfile> profiles) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ for (VideoProfile profile : profiles) {
+ proxies.add(VideoProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getFrameRate(),
+ profile.getWidth(),
+ profile.getHeight(),
+ profile.getProfile(),
+ VideoProfileProxy.BIT_DEPTH_8,
+ VideoProfile.YUV_420,
+ VideoProfile.HDR_NONE
+ ));
+ }
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> fromAudioProfiles(
+ @NonNull List<AudioProfile> profiles) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ for (AudioProfile profile : profiles) {
+ proxies.add(AudioProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getSampleRate(),
+ profile.getChannels(),
+ profile.getProfile()
+ ));
+ }
+ return proxies;
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatApi31Impl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
new file mode 100644
index 0000000..e69305b
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatApi33Impl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 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.compat;
+
+import android.media.EncoderProfiles;
+import android.media.EncoderProfiles.AudioProfile;
+import android.media.EncoderProfiles.VideoProfile;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(33)
+class EncoderProfilesProxyCompatApi33Impl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link EncoderProfiles}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull EncoderProfiles encoderProfiles) {
+ return ImmutableEncoderProfilesProxy.create(
+ encoderProfiles.getDefaultDurationSeconds(),
+ encoderProfiles.getRecommendedFileFormat(),
+ fromAudioProfiles(encoderProfiles.getAudioProfiles()),
+ fromVideoProfiles(encoderProfiles.getVideoProfiles())
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from a list of {@link VideoProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> fromVideoProfiles(
+ @NonNull List<VideoProfile> profiles) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ for (VideoProfile profile : profiles) {
+ proxies.add(VideoProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getFrameRate(),
+ profile.getWidth(),
+ profile.getHeight(),
+ profile.getProfile(),
+ profile.getBitDepth(),
+ profile.getChromaSubsampling(),
+ profile.getHdrFormat()
+ ));
+ }
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from a list of {@link AudioProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> fromAudioProfiles(
+ @NonNull List<AudioProfile> profiles) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ for (AudioProfile profile : profiles) {
+ proxies.add(AudioProfileProxy.create(
+ profile.getCodec(),
+ profile.getMediaType(),
+ profile.getBitrate(),
+ profile.getSampleRate(),
+ profile.getChannels(),
+ profile.getProfile()
+ ));
+ }
+ return proxies;
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatApi33Impl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
new file mode 100644
index 0000000..7e4b830
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/compat/EncoderProfilesProxyCompatBaseImpl.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023 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.compat;
+
+import static android.media.MediaRecorder.AudioEncoder.AAC;
+import static android.media.MediaRecorder.AudioEncoder.AAC_ELD;
+import static android.media.MediaRecorder.AudioEncoder.AMR_NB;
+import static android.media.MediaRecorder.AudioEncoder.AMR_WB;
+import static android.media.MediaRecorder.AudioEncoder.HE_AAC;
+import static android.media.MediaRecorder.AudioEncoder.OPUS;
+import static android.media.MediaRecorder.AudioEncoder.VORBIS;
+import static android.media.MediaRecorder.VideoEncoder.AV1;
+import static android.media.MediaRecorder.VideoEncoder.DOLBY_VISION;
+import static android.media.MediaRecorder.VideoEncoder.H263;
+import static android.media.MediaRecorder.VideoEncoder.H264;
+import static android.media.MediaRecorder.VideoEncoder.HEVC;
+import static android.media.MediaRecorder.VideoEncoder.MPEG_4_SP;
+import static android.media.MediaRecorder.VideoEncoder.VP8;
+import static android.media.MediaRecorder.VideoEncoder.VP9;
+
+import android.media.CamcorderProfile;
+import android.media.EncoderProfiles;
+import android.media.MediaCodecInfo;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class EncoderProfilesProxyCompatBaseImpl {
+
+ /** Creates an EncoderProfilesProxy instance from {@link CamcorderProfile}. */
+ @NonNull
+ public static EncoderProfilesProxy from(
+ @NonNull CamcorderProfile camcorderProfile) {
+ return ImmutableEncoderProfilesProxy.create(
+ camcorderProfile.duration,
+ camcorderProfile.fileFormat,
+ toAudioProfiles(camcorderProfile),
+ toVideoProfiles(camcorderProfile)
+ );
+ }
+
+ /** Creates VideoProfileProxy instances from {@link CamcorderProfile}. */
+ @NonNull
+ private static List<VideoProfileProxy> toVideoProfiles(
+ @NonNull CamcorderProfile camcorderProfile) {
+ List<VideoProfileProxy> proxies = new ArrayList<>();
+ proxies.add(VideoProfileProxy.create(
+ camcorderProfile.videoCodec,
+ getVideoCodecMimeType(camcorderProfile.videoCodec),
+ camcorderProfile.videoBitRate,
+ camcorderProfile.videoFrameRate,
+ camcorderProfile.videoFrameWidth,
+ camcorderProfile.videoFrameHeight,
+ EncoderProfilesProxy.CODEC_PROFILE_NONE,
+ VideoProfileProxy.BIT_DEPTH_8,
+ EncoderProfiles.VideoProfile.YUV_420,
+ EncoderProfiles.VideoProfile.HDR_NONE
+ ));
+ return proxies;
+ }
+
+ /** Creates AudioProfileProxy instances from {@link CamcorderProfile}. */
+ @NonNull
+ private static List<AudioProfileProxy> toAudioProfiles(
+ @NonNull CamcorderProfile camcorderProfile) {
+ List<AudioProfileProxy> proxies = new ArrayList<>();
+ proxies.add(AudioProfileProxy.create(
+ camcorderProfile.audioCodec,
+ getAudioCodecMimeType(camcorderProfile.audioCodec),
+ camcorderProfile.audioBitRate,
+ camcorderProfile.audioSampleRate,
+ camcorderProfile.audioChannels,
+ getRequiredAudioProfile(camcorderProfile.audioCodec)
+ ));
+ return proxies;
+ }
+
+ /**
+ * Returns a mime-type string for the given video codec type.
+ *
+ * @return A mime-type string or {@link VideoProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+ * {@link MediaRecorder.VideoEncoder#DEFAULT}, as this type is under-defined and cannot be
+ * resolved to a specific mime type without more information.
+ */
+ @NonNull
+ private static String getVideoCodecMimeType(
+ @VideoProfileProxy.VideoEncoder int codec) {
+ switch (codec) {
+ // Mime-type definitions taken from
+ // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+ case H263:
+ return MediaFormat.MIMETYPE_VIDEO_H263;
+ case H264:
+ return MediaFormat.MIMETYPE_VIDEO_AVC;
+ case HEVC:
+ return MediaFormat.MIMETYPE_VIDEO_HEVC;
+ case VP8:
+ return MediaFormat.MIMETYPE_VIDEO_VP8;
+ case MPEG_4_SP:
+ return MediaFormat.MIMETYPE_VIDEO_MPEG4;
+ case VP9:
+ return MediaFormat.MIMETYPE_VIDEO_VP9;
+ case DOLBY_VISION:
+ return MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION;
+ case AV1:
+ return MediaFormat.MIMETYPE_VIDEO_AV1;
+ case MediaRecorder.VideoEncoder.DEFAULT:
+ break;
+ }
+
+ return VideoProfileProxy.MEDIA_TYPE_NONE;
+ }
+
+ /**
+ * Returns a mime-type string for the given audio codec type.
+ *
+ * @return A mime-type string or {@link AudioProfileProxy#MEDIA_TYPE_NONE} if the codec type is
+ * {@link android.media.MediaRecorder.AudioEncoder#DEFAULT}, as this type is under-defined
+ * and cannot be resolved to a specific mime type without more information.
+ */
+ @NonNull
+ private static String getAudioCodecMimeType(@AudioProfileProxy.AudioEncoder int codec) {
+ // Mime-type definitions taken from
+ // frameworks/av/media/libstagefright/foundation/MediaDefs.cpp
+ switch (codec) {
+ case AAC: // Should use aac-profile LC
+ case HE_AAC: // Should use aac-profile HE
+ case AAC_ELD: // Should use aac-profile ELD
+ return MediaFormat.MIMETYPE_AUDIO_AAC;
+ case AMR_NB:
+ return MediaFormat.MIMETYPE_AUDIO_AMR_NB;
+ case AMR_WB:
+ return MediaFormat.MIMETYPE_AUDIO_AMR_WB;
+ case OPUS:
+ return MediaFormat.MIMETYPE_AUDIO_OPUS;
+ case VORBIS:
+ return MediaFormat.MIMETYPE_AUDIO_VORBIS;
+ case MediaRecorder.AudioEncoder.DEFAULT:
+ break;
+ }
+
+ return AudioProfileProxy.MEDIA_TYPE_NONE;
+ }
+
+ /**
+ * Returns the required audio profile for the given audio encoder.
+ *
+ * <p>For example, this can be used to differentiate between AAC encoders
+ * {@link android.media.MediaRecorder.AudioEncoder#AAC},
+ * {@link android.media.MediaRecorder.AudioEncoder#AAC_ELD},
+ * and {@link android.media.MediaRecorder.AudioEncoder#HE_AAC}.
+ * Should be used with the {@link MediaCodecInfo.CodecProfileLevel#profile} field.
+ *
+ * @return The profile required by the audio codec. If no profile is required, returns
+ * {@link EncoderProfilesProxy#CODEC_PROFILE_NONE}.
+ */
+ private static int getRequiredAudioProfile(@AudioProfileProxy.AudioEncoder int codec) {
+ switch (codec) {
+ case AAC:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectLC;
+ case AAC_ELD:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectELD;
+ case HE_AAC:
+ return MediaCodecInfo.CodecProfileLevel.AACObjectHE;
+ default:
+ return EncoderProfilesProxy.CODEC_PROFILE_NONE;
+ }
+ }
+
+ // Class should not be instantiated.
+ private EncoderProfilesProxyCompatBaseImpl() {
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
new file mode 100644
index 0000000..cdb5a87
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/quirk/ProfileResolutionQuirk.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2023 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.quirk;
+
+import android.media.EncoderProfiles;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * A Quirk interface which denotes that CameraX should validate video resolutions returned from
+ * {@link EncoderProfiles} instead of using them directly.
+ *
+ * <p>Subclasses of this quirk should provide a list of supported resolutions for CameraX to
+ * verify.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface ProfileResolutionQuirk extends Quirk {
+
+ /** Returns a list of supported resolutions. */
+ @NonNull
+ List<Size> getSupportedResolutions();
+}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
new file mode 100644
index 0000000..57b9338
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/EncoderProfilesResolutionValidatorTest.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2023 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.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil
+import com.google.common.truth.Truth.assertThat
+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)
+class EncoderProfilesResolutionValidatorTest {
+
+ @Test
+ fun noQuirk_alwaysValid() {
+ val validator = EncoderProfilesResolutionValidator(null)
+
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isTrue()
+ }
+
+ @Test
+ fun hasQuirk_shouldCheckSupportedResolutions() {
+ val quirk = createFakeProfileResolutionQuirk(
+ supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+ )
+ val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_2160P)).isTrue()
+ assertThat(validator.hasValidVideoResolution(EncoderProfilesUtil.PROFILES_720P)).isFalse()
+ }
+
+ @Test
+ fun nullProfile_notValid() {
+ val quirk = createFakeProfileResolutionQuirk(
+ supportedResolution = arrayOf(EncoderProfilesUtil.RESOLUTION_2160P)
+ )
+ val validator = EncoderProfilesResolutionValidator(listOf(quirk))
+
+ assertThat(validator.hasValidVideoResolution(null)).isFalse()
+ }
+
+ private fun createFakeProfileResolutionQuirk(
+ supportedResolution: Array<Size> = emptyArray()
+ ): ProfileResolutionQuirk {
+ return FakeQuirk(supportedResolution)
+ }
+
+ class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+ override fun getSupportedResolutions(): MutableList<Size> {
+ return supportedResolutions.toMutableList()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..198bc4c
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/ResolutionValidatedEncoderProfilesProviderTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2023 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.media.CamcorderProfile.QUALITY_1080P
+import android.media.CamcorderProfile.QUALITY_2160P
+import android.media.CamcorderProfile.QUALITY_480P
+import android.media.CamcorderProfile.QUALITY_720P
+import android.os.Build
+import android.util.Size
+import androidx.camera.core.impl.quirk.ProfileResolutionQuirk
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_1080P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_2160P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_480P
+import androidx.camera.testing.EncoderProfilesUtil.PROFILES_720P
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_1080P
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private val PAIR_2160 = Pair(QUALITY_2160P, PROFILES_2160P)
+private val PAIR_1080 = Pair(QUALITY_1080P, PROFILES_1080P)
+private val PAIR_720 = Pair(QUALITY_720P, PROFILES_720P)
+private val PAIR_480 = Pair(QUALITY_480P, PROFILES_480P)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class ResolutionValidatedEncoderProfilesProviderTest {
+
+ private val defaultProvider = createFakeEncoderProfilesProvider(
+ arrayOf(PAIR_2160, PAIR_1080, PAIR_720, PAIR_480)
+ )
+
+ @Test
+ fun hasNoProfile_canNotGetProfiles() {
+ val quirks = createQuirksWithProfileResolutionQuirk(
+ supportedResolution = arrayOf(RESOLUTION_1080P)
+ )
+ val emptyProvider = createFakeEncoderProfilesProvider()
+ val provider = ResolutionValidatedEncoderProfilesProvider(emptyProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+ assertThat(provider.getAll(QUALITY_2160P)).isNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNull()
+ }
+
+ @Test
+ fun hasQuirk_canOnlyGetSupportedProfiles() {
+ val quirks = createQuirksWithProfileResolutionQuirk(
+ supportedResolution = arrayOf(RESOLUTION_1080P)
+ )
+ val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_720P)).isFalse()
+ assertThat(provider.hasProfile(QUALITY_480P)).isFalse()
+ assertThat(provider.getAll(QUALITY_2160P)).isNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNull()
+ }
+
+ @Test
+ fun hasNoQuirk_canGetProfiles() {
+ val quirks = Quirks(emptyList())
+ val provider = ResolutionValidatedEncoderProfilesProvider(defaultProvider, quirks)
+
+ assertThat(provider.hasProfile(QUALITY_2160P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_720P)).isTrue()
+ assertThat(provider.hasProfile(QUALITY_480P)).isTrue()
+ assertThat(provider.getAll(QUALITY_2160P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_1080P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_720P)).isNotNull()
+ assertThat(provider.getAll(QUALITY_480P)).isNotNull()
+ }
+
+ private fun createFakeEncoderProfilesProvider(
+ qualityToProfilesPairs: Array<Pair<Int, EncoderProfilesProxy>> = emptyArray()
+ ): EncoderProfilesProvider {
+ return FakeEncoderProfilesProvider.Builder().also { builder ->
+ for (pair in qualityToProfilesPairs) {
+ builder.add(pair.first, pair.second)
+ }
+ }.build()
+ }
+
+ private fun createQuirksWithProfileResolutionQuirk(
+ supportedResolution: Array<Size> = emptyArray()
+ ): Quirks {
+ return Quirks(listOf(FakeQuirk(supportedResolution)))
+ }
+
+ class FakeQuirk(private val supportedResolutions: Array<Size>) : ProfileResolutionQuirk {
+
+ override fun getSupportedResolutions(): MutableList<Size> {
+ return supportedResolutions.toMutableList()
+ }
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
new file mode 100644
index 0000000..4c84b93
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2023 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.EncoderProfiles;
+import android.media.MediaFormat;
+import android.media.MediaRecorder;
+import android.util.Size;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.AudioProfileProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.Collections;
+
+/**
+ * Utility methods for testing {@link EncoderProfiles} related classes, including predefined
+ * resolutions, attributes and {@link EncoderProfilesProxy}, which can be used directly on the
+ * unit tests.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public final class EncoderProfilesUtil {
+
+ /** Resolution for QCIF. */
+ public static final Size RESOLUTION_QCIF = new Size(176, 144);
+ /** Resolution for QVGA. */
+ public static final Size RESOLUTION_QVGA = new Size(320, 240);
+ /** Resolution for CIF. */
+ public static final Size RESOLUTION_CIF = new Size(352, 288);
+ /** Resolution for VGA. */
+ public static final Size RESOLUTION_VGA = new Size(640, 480);
+ /** Resolution for 480P. */
+ public static final Size RESOLUTION_480P = new Size(720, 480); /* 640, 704 or 720 x 480 */
+ /** Resolution for 720P. */
+ public static final Size RESOLUTION_720P = new Size(1280, 720);
+ /** Resolution for 1080P. */
+ public static final Size RESOLUTION_1080P = new Size(1920, 1080); /* 1920 x 1080 or 1088 */
+ /** Resolution for 2K. */
+ public static final Size RESOLUTION_2K = new Size(2048, 1080);
+ /** Resolution for QHD. */
+ public static final Size RESOLUTION_QHD = new Size(2560, 1440);
+ /** Resolution for 2160P. */
+ public static final Size RESOLUTION_2160P = new Size(3840, 2160);
+ /** Resolution for 4KDCI. */
+ public static final Size RESOLUTION_4KDCI = new Size(4096, 2160);
+
+ /** Default duration. */
+ public static final int DEFAULT_DURATION = 30;
+ /** Default output format. */
+ public static final int DEFAULT_OUTPUT_FORMAT = MediaRecorder.OutputFormat.MPEG_4;
+ /** Default video codec. */
+ public static final int DEFAULT_VIDEO_CODEC = MediaRecorder.VideoEncoder.H264;
+ /** Default media type. */
+ public static final String DEFAULT_VIDEO_MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
+ /** Default video bitrate. */
+ public static final int DEFAULT_VIDEO_BITRATE = 8 * 1024 * 1024;
+ /** Default video frame rate. */
+ public static final int DEFAULT_VIDEO_FRAME_RATE = 30;
+ /** Default video code profile. */
+ public static final int DEFAULT_VIDEO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+ /** Default bit depth. */
+ public static final int DEFAULT_VIDEO_BIT_DEPTH = VideoProfileProxy.BIT_DEPTH_8;
+ /** Default chroma subsampling. */
+ public static final int DEFAULT_VIDEO_CHROMA_SUBSAMPLING = EncoderProfiles.VideoProfile.YUV_420;
+ /** Default hdr format. */
+ public static final int DEFAULT_VIDEO_HDR_FORMAT = EncoderProfiles.VideoProfile.HDR_NONE;
+ /** Default audio codec. */
+ public static final int DEFAULT_AUDIO_CODEC = MediaRecorder.AudioEncoder.AAC;
+ /** Default media type. */
+ public static final String DEFAULT_AUDIO_MEDIA_TYPE = MediaFormat.MIMETYPE_AUDIO_AAC;
+ /** Default audio bitrate. */
+ public static final int DEFAULT_AUDIO_BITRATE = 192_000;
+ /** Default audio sample rate. */
+ public static final int DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
+ /** Default channel count. */
+ public static final int DEFAULT_AUDIO_CHANNELS = 1;
+ /** Default audio code profile. */
+ public static final int DEFAULT_AUDIO_PROFILE = EncoderProfilesProxy.CODEC_PROFILE_NONE;
+
+ public static final EncoderProfilesProxy PROFILES_QCIF = createFakeEncoderProfilesProxy(
+ RESOLUTION_QCIF.getWidth(),
+ RESOLUTION_QCIF.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_QVGA = createFakeEncoderProfilesProxy(
+ RESOLUTION_QVGA.getWidth(),
+ RESOLUTION_QVGA.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_CIF = createFakeEncoderProfilesProxy(
+ RESOLUTION_CIF.getWidth(),
+ RESOLUTION_CIF.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_VGA = createFakeEncoderProfilesProxy(
+ RESOLUTION_VGA.getWidth(),
+ RESOLUTION_VGA.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_480P = createFakeEncoderProfilesProxy(
+ RESOLUTION_480P.getWidth(),
+ RESOLUTION_480P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_720P = createFakeEncoderProfilesProxy(
+ RESOLUTION_720P.getWidth(),
+ RESOLUTION_720P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_1080P = createFakeEncoderProfilesProxy(
+ RESOLUTION_1080P.getWidth(),
+ RESOLUTION_1080P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_2K = createFakeEncoderProfilesProxy(
+ RESOLUTION_2K.getWidth(),
+ RESOLUTION_2K.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_QHD = createFakeEncoderProfilesProxy(
+ RESOLUTION_QHD.getWidth(),
+ RESOLUTION_QHD.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_2160P = createFakeEncoderProfilesProxy(
+ RESOLUTION_2160P.getWidth(),
+ RESOLUTION_2160P.getHeight()
+ );
+
+ public static final EncoderProfilesProxy PROFILES_4KDCI = createFakeEncoderProfilesProxy(
+ RESOLUTION_4KDCI.getWidth(),
+ RESOLUTION_4KDCI.getHeight()
+ );
+
+ /** A utility method to create an EncoderProfilesProxy with some default values. */
+ @NonNull
+ public static EncoderProfilesProxy createFakeEncoderProfilesProxy(
+ int videoFrameWidth,
+ int videoFrameHeight
+ ) {
+ VideoProfileProxy videoProfile = VideoProfileProxy.create(
+ DEFAULT_VIDEO_CODEC,
+ DEFAULT_VIDEO_MEDIA_TYPE,
+ DEFAULT_VIDEO_BITRATE,
+ DEFAULT_VIDEO_FRAME_RATE,
+ videoFrameWidth,
+ videoFrameHeight,
+ DEFAULT_VIDEO_PROFILE,
+ DEFAULT_VIDEO_BIT_DEPTH,
+ DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
+ DEFAULT_VIDEO_HDR_FORMAT
+ );
+ AudioProfileProxy audioProfile = AudioProfileProxy.create(
+ DEFAULT_AUDIO_CODEC,
+ DEFAULT_AUDIO_MEDIA_TYPE,
+ DEFAULT_AUDIO_BITRATE,
+ DEFAULT_AUDIO_SAMPLE_RATE,
+ DEFAULT_AUDIO_CHANNELS,
+ DEFAULT_AUDIO_PROFILE
+ );
+
+ return ImmutableEncoderProfilesProxy.create(
+ DEFAULT_DURATION,
+ DEFAULT_OUTPUT_FORMAT,
+ Collections.singletonList(audioProfile),
+ Collections.singletonList(videoProfile)
+ );
+ }
+
+ // This class is not instantiable.
+ private EncoderProfilesUtil() {
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
new file mode 100644
index 0000000..a4d1e46
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeEncoderProfilesProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.fakes;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A fake implementation of the {@link EncoderProfilesProvider} and used for test.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class FakeEncoderProfilesProvider implements EncoderProfilesProvider {
+
+ private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap;
+
+ FakeEncoderProfilesProvider(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+ mQualityToProfileMap = qualityToProfileMap;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean hasProfile(int quality) {
+ return mQualityToProfileMap.get(quality) != null;
+ }
+
+ /** {@inheritDoc} */
+ @Nullable
+ @Override
+ public EncoderProfilesProxy getAll(int quality) {
+ return mQualityToProfileMap.get(quality);
+ }
+
+ /**
+ * The builder to create a FakeEncoderProfilesProvider instance.
+ */
+ @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+ public static class Builder {
+
+ private final Map<Integer, EncoderProfilesProxy> mQualityToProfileMap = new HashMap<>();
+
+ /**
+ * Adds a quality and its corresponding profiles.
+ */
+ @NonNull
+ public Builder add(int quality, @NonNull EncoderProfilesProxy profiles) {
+ mQualityToProfileMap.put(quality, profiles);
+ return this;
+ }
+
+ /**
+ * Adds qualities and their corresponding profiles.
+ */
+ @NonNull
+ public Builder addAll(@NonNull Map<Integer, EncoderProfilesProxy> qualityToProfileMap) {
+ mQualityToProfileMap.putAll(qualityToProfileMap);
+ return this;
+ }
+
+ /** Builds the FakeEncoderProfilesProvider instance. */
+ @NonNull
+ public FakeEncoderProfilesProvider build() {
+ return new FakeEncoderProfilesProvider(mQualityToProfileMap);
+ }
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FallbackStrategy.java b/camera/camera-video/src/main/java/androidx/camera/video/FallbackStrategy.java
index e45ba08..ad3e94f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/FallbackStrategy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FallbackStrategy.java
@@ -73,7 +73,7 @@
}
/**
- * Returns a fallback strategy that will choose the quality that is closest to and higher
+ * Returns a fallback strategy that will choose the quality that is closest to and lower
* than the input quality.
*/
@NonNull
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxy.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxy.java
new file mode 100644
index 0000000..025c0c2
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxy.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 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.video.internal;
+
+import static java.util.Collections.unmodifiableList;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.core.util.Preconditions;
+
+import com.google.auto.value.AutoValue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * VideoValidatedEncoderProfilesProxy is an implementation of {@link EncoderProfilesProxy} that
+ * guarantees to provide video information.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@AutoValue
+public abstract class VideoValidatedEncoderProfilesProxy implements EncoderProfilesProxy {
+
+ /** Creates a VideoValidatedEncoderProfilesProxy instance from {@link EncoderProfilesProxy}. */
+ @NonNull
+ public static VideoValidatedEncoderProfilesProxy from(@NonNull EncoderProfilesProxy profiles) {
+ return create(
+ profiles.getDefaultDurationSeconds(),
+ profiles.getRecommendedFileFormat(),
+ profiles.getAudioProfiles(),
+ profiles.getVideoProfiles()
+ );
+ }
+
+ /** Creates a VideoValidatedEncoderProfilesProxy instance. */
+ @NonNull
+ public static VideoValidatedEncoderProfilesProxy create(
+ int defaultDurationSeconds,
+ int recommendedFileFormat,
+ @NonNull List<AudioProfileProxy> audioProfiles,
+ @NonNull List<VideoProfileProxy> videoProfiles) {
+ Preconditions.checkArgument(!videoProfiles.isEmpty(),
+ "Should contain at least one VideoProfile.");
+ VideoProfileProxy defaultVideoProfile = videoProfiles.get(0);
+
+ AudioProfileProxy defaultAudioProfile = null;
+ if (!audioProfiles.isEmpty()) {
+ defaultAudioProfile = audioProfiles.get(0);
+ }
+
+ return new AutoValue_VideoValidatedEncoderProfilesProxy(
+ defaultDurationSeconds,
+ recommendedFileFormat,
+ unmodifiableList(new ArrayList<>(audioProfiles)),
+ unmodifiableList(new ArrayList<>(videoProfiles)),
+ defaultAudioProfile,
+ defaultVideoProfile
+ );
+ }
+
+ /** Returns the default {@link AudioProfileProxy} or null if not existed. */
+ @Nullable
+ public abstract AudioProfileProxy getDefaultAudioProfile();
+
+ /** Returns the default {@link VideoProfileProxy}. */
+ @NonNull
+ public abstract VideoProfileProxy getDefaultVideoProfile();
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxyTest.kt
new file mode 100644
index 0000000..b853ba7
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/VideoValidatedEncoderProfilesProxyTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2023 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.video.internal
+
+import android.os.Build
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
+import androidx.camera.testing.EncoderProfilesUtil
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+private const val DEFAULT_WIDTH = 1920
+private const val DEFAULT_HEIGHT = 1080
+private val DEFAULT_VIDEO_PROFILE = VideoProfileProxy.create(
+ EncoderProfilesUtil.DEFAULT_VIDEO_CODEC,
+ EncoderProfilesUtil.DEFAULT_VIDEO_MEDIA_TYPE,
+ EncoderProfilesUtil.DEFAULT_VIDEO_BITRATE,
+ EncoderProfilesUtil.DEFAULT_VIDEO_FRAME_RATE,
+ DEFAULT_WIDTH,
+ DEFAULT_HEIGHT,
+ EncoderProfilesUtil.DEFAULT_VIDEO_PROFILE,
+ EncoderProfilesUtil.DEFAULT_VIDEO_BIT_DEPTH,
+ EncoderProfilesUtil.DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
+ EncoderProfilesUtil.DEFAULT_VIDEO_HDR_FORMAT
+)
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class VideoValidatedEncoderProfilesProxyTest {
+
+ @Test
+ fun createFromEncoderProfilesProxy() {
+ val profiles = EncoderProfilesUtil.PROFILES_1080P
+ val validatedProfiles = VideoValidatedEncoderProfilesProxy.from(profiles)
+
+ assertThat(validatedProfiles.recommendedFileFormat)
+ .isEqualTo(profiles.recommendedFileFormat)
+ assertThat(validatedProfiles.defaultDurationSeconds)
+ .isEqualTo(profiles.defaultDurationSeconds)
+ assertThat(validatedProfiles.audioProfiles.size).isEqualTo(profiles.audioProfiles.size)
+ assertThat(validatedProfiles.videoProfiles.size).isEqualTo(profiles.videoProfiles.size)
+ assertThat(validatedProfiles.defaultAudioProfile).isNotNull()
+ assertThat(validatedProfiles.defaultVideoProfile).isNotNull()
+ assertThat(validatedProfiles.audioProfiles[0].codec)
+ .isEqualTo(profiles.audioProfiles[0].codec)
+ assertThat(validatedProfiles.audioProfiles[0].mediaType)
+ .isEqualTo(profiles.audioProfiles[0].mediaType)
+ assertThat(validatedProfiles.audioProfiles[0].bitrate)
+ .isEqualTo(profiles.audioProfiles[0].bitrate)
+ assertThat(validatedProfiles.audioProfiles[0].sampleRate)
+ .isEqualTo(profiles.audioProfiles[0].sampleRate)
+ assertThat(validatedProfiles.audioProfiles[0].channels)
+ .isEqualTo(profiles.audioProfiles[0].channels)
+ assertThat(validatedProfiles.audioProfiles[0].profile)
+ .isEqualTo(profiles.audioProfiles[0].profile)
+ assertThat(validatedProfiles.videoProfiles[0].codec)
+ .isEqualTo(profiles.videoProfiles[0].codec)
+ assertThat(validatedProfiles.videoProfiles[0].mediaType)
+ .isEqualTo(profiles.videoProfiles[0].mediaType)
+ assertThat(validatedProfiles.videoProfiles[0].bitrate)
+ .isEqualTo(profiles.videoProfiles[0].bitrate)
+ assertThat(validatedProfiles.videoProfiles[0].frameRate)
+ .isEqualTo(profiles.videoProfiles[0].frameRate)
+ assertThat(validatedProfiles.videoProfiles[0].width)
+ .isEqualTo(profiles.videoProfiles[0].width)
+ assertThat(validatedProfiles.videoProfiles[0].height)
+ .isEqualTo(profiles.videoProfiles[0].height)
+ assertThat(validatedProfiles.videoProfiles[0].profile)
+ .isEqualTo(profiles.videoProfiles[0].profile)
+ assertThat(validatedProfiles.videoProfiles[0].bitDepth)
+ .isEqualTo(profiles.videoProfiles[0].bitDepth)
+ assertThat(validatedProfiles.videoProfiles[0].chromaSubsampling)
+ .isEqualTo(profiles.videoProfiles[0].chromaSubsampling)
+ assertThat(validatedProfiles.videoProfiles[0].hdrFormat)
+ .isEqualTo(profiles.videoProfiles[0].hdrFormat)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun create_throwsException_whenVideoProfilesIsEmpty() {
+ VideoValidatedEncoderProfilesProxy.create(
+ EncoderProfilesUtil.DEFAULT_DURATION,
+ EncoderProfilesUtil.DEFAULT_OUTPUT_FORMAT,
+ emptyList(),
+ emptyList()
+ )
+ }
+
+ @Test
+ fun create_withEmptyAudioProfiles() {
+ val validatedProfiles = VideoValidatedEncoderProfilesProxy.create(
+ EncoderProfilesUtil.DEFAULT_DURATION,
+ EncoderProfilesUtil.DEFAULT_OUTPUT_FORMAT,
+ emptyList(),
+ listOf(DEFAULT_VIDEO_PROFILE)
+ )
+ assertThat(validatedProfiles.defaultAudioProfile).isNull()
+ }
+}
\ No newline at end of file
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 2ed53fc..3602e67 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
@@ -1435,6 +1435,7 @@
return;
}
mVideoCaptureQuality = targetQuality;
+ unbindVideoAndRecreate();
startCameraAndTrackStates();
}
@@ -1450,6 +1451,16 @@
return mVideoCaptureQuality;
}
+ /**
+ * Unbinds VideoCapture and recreate with the latest parameters.
+ */
+ private void unbindVideoAndRecreate() {
+ if (isCameraInitialized()) {
+ mCameraProvider.unbind(mVideoCapture);
+ }
+ mVideoCapture = createNewVideoCapture();
+ }
+
private VideoCapture<Recorder> createNewVideoCapture() {
return VideoCapture.withOutput(generateVideoCaptureRecorder(mVideoCaptureQuality));
}
@@ -2002,11 +2013,10 @@
mCameraProvider.unbind(mImageAnalysis);
}
- // TODO: revert aosp/2280599 to reuse VideoCapture when VideoCapture supports reuse.
- mCameraProvider.unbind(mVideoCapture);
if (isVideoCaptureEnabled()) {
- mVideoCapture = createNewVideoCapture();
builder.addUseCase(mVideoCapture);
+ } else {
+ mCameraProvider.unbind(mVideoCapture);
}
builder.setViewPort(mViewPort);
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt
index 4d6e879..e75252f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/CardTest.kt
@@ -117,7 +117,7 @@
}
rule.onNodeWithTag("card")
.assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("0")
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
index b9bdd17..bfb6224 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -34,6 +34,10 @@
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertWidthIsAtLeast
@@ -71,6 +75,7 @@
}
rule.onNodeWithTag("myButton")
+ .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
.assertIsEnabled()
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt
index 99ffbe5..f288a88 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SurfaceTest.kt
@@ -218,7 +218,7 @@
}
rule.onNodeWithTag("surface")
.assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Button))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("0")
@@ -364,7 +364,7 @@
}
rule.onNodeWithTag("surface")
.assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Tab))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("false")
@@ -468,7 +468,7 @@
}
rule.onNodeWithTag("surface")
.assertHasClickAction()
- .assert(SemanticsMatcher.expectValue(SemanticsProperties.Role, Role.Switch))
+ .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
.assertIsEnabled()
// since we merge descendants we should have text on the same node
.assertTextEquals("false")
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index 496ed96..b27bb71 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -48,6 +48,9 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collect
@@ -103,7 +106,7 @@
val contentColor by colors.contentColor(enabled)
Surface(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.semantics { role = Role.Button },
enabled = enabled,
shape = shape,
color = colors.backgroundColor(enabled).value,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Chip.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Chip.kt
index 437d88d..d151712 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Chip.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Chip.kt
@@ -99,7 +99,7 @@
val contentColor by colors.contentColor(enabled)
Surface(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.semantics { role = Role.Button },
enabled = enabled,
shape = shape,
color = colors.backgroundColor(enabled).value,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index 6225e3a..8995a68 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -44,6 +44,9 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collect
@@ -88,7 +91,7 @@
) {
Surface(
onClick = onClick,
- modifier = modifier,
+ modifier = modifier.semantics { role = Role.Button },
shape = shape,
color = backgroundColor,
contentColor = contentColor,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
index 9aef3d8..895c713 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
@@ -178,9 +178,8 @@
* that doesn't require [onClick] param.
*
* 7) Semantics for clicks. Just like with [Modifier.clickable], clickable version of Surface will
- * produce semantics to indicate that it is clicked. Also, by default, accessibility services will
- * describe the element as [Role.Button]. You may change this by passing a desired [Role] with a
- * [Modifier.semantics].
+ * produce semantics to indicate that it is clicked. No semantic role is set by default, you
+ * may specify one by passing a desired [Role] with a [Modifier.semantics].
*
* @sample androidx.compose.material.samples.ClickableSurfaceSample
*
@@ -243,7 +242,6 @@
interactionSource = interactionSource,
indication = rememberRipple(),
enabled = enabled,
- role = Role.Button,
onClick = onClick
),
propagateMinConstraints = true
@@ -292,9 +290,8 @@
* that doesn't require [onClick] param.
*
* 7) Semantics for selection. Just like with [Modifier.selectable], selectable version of Surface
- * will produce semantics to indicate that it is selected. Also, by default, accessibility services
- * will describe the element as [Role.Tab]. You may change this by passing a desired [Role] with a
- * [Modifier.semantics].
+ * will produce semantics to indicate that it is selected. No semantic role is set by default, you
+ * may specify one by passing a desired [Role] with a [Modifier.semantics].
*
* @sample androidx.compose.material.samples.SelectableSurfaceSample
*
@@ -360,7 +357,6 @@
interactionSource = interactionSource,
indication = rememberRipple(),
enabled = enabled,
- role = Role.Tab,
onClick = onClick
),
propagateMinConstraints = true
@@ -409,9 +405,8 @@
* handling, consider using a Surface function that doesn't require [onCheckedChange] param.
*
* 7) Semantics for toggle. Just like with [Modifier.toggleable], toggleable version of Surface
- * will produce semantics to indicate that it is checked. Also, by default, accessibility services
- * will describe the element as [Role.Switch]. You may change this by passing a desired [Role] with
- * a [Modifier.semantics].
+ * will produce semantics to indicate that it is checked. No semantic role is set by default, you
+ * may specify one by passing a desired [Role] with a [Modifier.semantics].
*
* @sample androidx.compose.material.samples.ToggleableSurfaceSample
*
@@ -477,7 +472,6 @@
interactionSource = interactionSource,
indication = rememberRipple(),
enabled = enabled,
- role = Role.Switch,
onValueChange = onCheckedChange
),
propagateMinConstraints = true
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 4b043b1..8ba8b89 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -16,13 +16,13 @@
import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryType
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("AndroidXPlugin")
id("com.android.library")
id("AndroidXComposePlugin")
+ id("AndroidXPaparazziPlugin")
}
AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
index ed385e3..47dd16b 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/TabTest.kt
@@ -17,8 +17,10 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
@@ -630,7 +632,7 @@
}
@Test
- fun LeadingIconTabRow_selectNewTab() {
+ fun leadingIconTabRow_selectNewTab() {
rule
.setMaterialContent(lightColorScheme()) {
LeadingIconTabs()
@@ -745,7 +747,8 @@
val indicator = @Composable { tabPositions: List<TabPosition> ->
TabRowDefaults.Indicator(
- Modifier.tabIndicatorOffset(tabPositions[state])
+ Modifier
+ .tabIndicatorOffset(tabPositions[state])
.testTag("indicator")
)
}
@@ -866,4 +869,68 @@
}
.assertHeightIsAtLeast(100.dp)
}
+
+ @Test
+ fun tabRow_layoutHeightRespected() {
+ var height by mutableStateOf(0.dp)
+ rule
+ .setMaterialContent(lightColorScheme()) {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3")
+ Column(
+ Modifier
+ .heightIn(max = height)
+ .testTag("Tabs")
+ ) {
+ TabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(text = title) }
+ )
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("Tabs").assertHeightIsEqualTo(height)
+
+ height = 40.dp
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("Tabs").assertHeightIsEqualTo(height)
+ }
+
+ @Test
+ fun scrollableTabRow_layoutHeightRespected() {
+ var height by mutableStateOf(0.dp)
+ rule
+ .setMaterialContent(lightColorScheme()) {
+ var state by remember { mutableStateOf(0) }
+ val titles = listOf("Tab 1", "Tab 2", "Tab 3")
+ Column(
+ Modifier
+ .heightIn(max = height)
+ .testTag("Tabs")
+ ) {
+ ScrollableTabRow(selectedTabIndex = state) {
+ titles.forEachIndexed { index, title ->
+ Tab(
+ selected = state == index,
+ onClick = { state = index },
+ text = { Text(text = title) }
+ )
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("Tabs").assertHeightIsEqualTo(height)
+
+ height = 40.dp
+ rule.waitForIdle()
+
+ rule.onNodeWithTag("Tabs").assertHeightIsEqualTo(height)
+ }
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
index 77881a5..043b2aa 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt
@@ -157,7 +157,8 @@
constraints.copy(
minWidth = tabWidth,
maxWidth = tabWidth,
- minHeight = tabRowHeight
+ minHeight = tabRowHeight,
+ maxHeight = tabRowHeight,
)
)
}
@@ -265,7 +266,11 @@
maxOf(curr, measurable.maxIntrinsicHeight(Constraints.Infinity))
}
- val tabConstraints = constraints.copy(minWidth = minTabWidth, minHeight = layoutHeight)
+ val tabConstraints = constraints.copy(
+ minWidth = minTabWidth,
+ minHeight = layoutHeight,
+ maxHeight = layoutHeight,
+ )
val tabPlaceables = tabMeasurables
.map { it.measure(tabConstraints) }
diff --git a/compose/material3/material3/src/test/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt b/compose/material3/material3/src/test/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
new file mode 100644
index 0000000..e4737d5
--- /dev/null
+++ b/compose/material3/material3/src/test/kotlin/androidx/compose/material3/ButtonPaparazziTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.material3
+
+import androidx.testutils.paparazzi.androidxPaparazzi
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ButtonPaparazziTest {
+ @get:Rule
+ val paparazzi = androidxPaparazzi()
+
+ @Test
+ fun default_button_light_theme() {
+ paparazzi.snapshot {
+ MaterialTheme(lightColorScheme()) {
+ Surface {
+ Button(onClick = { }) {
+ Text("Button")
+ }
+ }
+ }
+ }
+ }
+
+ @Test
+ fun default_button_dark_theme() {
+ paparazzi.snapshot {
+ MaterialTheme(darkColorScheme()) {
+ Surface {
+ Button(onClick = { }) {
+ Text("Button")
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 5868acd..c10ff1a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,6 +6,8 @@
org.gradle.welcome=never
# Disabled due to https://github.com/gradle/gradle/issues/18626
# org.gradle.vfs.watch=true
+# Reenabled in gradlew, but disabled in Studio until these errors become shown (b/268380971) or computed more quickly (https://github.com/gradle/gradle/issues/23272)
+org.gradle.dependency.verification=off
org.gradle.dependency.verification.console=verbose
org.gradle.unsafe.configuration-cache=true
org.gradle.unsafe.configuration-cache-problems=warn
diff --git a/gradlew b/gradlew
index 9b54b57..11139b9 100755
--- a/gradlew
+++ b/gradlew
@@ -396,7 +396,13 @@
RETURN_VALUE=0
PROJECT_CACHE_DIR_ARGUMENT="--project-cache-dir $OUT_DIR/gradle-project-cache"
- if $wrapper "$JAVACMD" "${JVM_OPTS[@]}" $TMPDIR_ARG -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain $HOME_SYSTEM_PROPERTY_ARGUMENT $TMPDIR_ARG $PROJECT_CACHE_DIR_ARGUMENT "$ORG_GRADLE_JVMARGS" "$@"; then
+ # Disabled in Studio until these errors become shown (b/268380971) or computed more quickly (https://github.com/gradle/gradle/issues/23272)
+ if [[ " ${@} " =~ " --dependency-verification=" ]]; then
+ VERIFICATION_ARGUMENT="" # already specified by caller
+ else
+ VERIFICATION_ARGUMENT=--dependency-verification=strict
+ fi
+ if $wrapper "$JAVACMD" "${JVM_OPTS[@]}" $TMPDIR_ARG -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain $HOME_SYSTEM_PROPERTY_ARGUMENT $TMPDIR_ARG $PROJECT_CACHE_DIR_ARGUMENT $VERIFICATION_ARGUMENT "$ORG_GRADLE_JVMARGS" "$@"; then
RETURN_VALUE=0
else
# Print AndroidX-specific help message if build fails
diff --git a/testutils/testutils-paparazzi/build.gradle b/testutils/testutils-paparazzi/build.gradle
index 4128c88..aeb9d21 100644
--- a/testutils/testutils-paparazzi/build.gradle
+++ b/testutils/testutils-paparazzi/build.gradle
@@ -25,7 +25,12 @@
BundleInsideHelper.forInsideJar(
project,
"com.google.protobuf",
- "androidx.testutils.paparazzi.protobuf"
+ // b/268288856 untangle this mess.
+ // Currently this has to match test/screenshot/screenshot/build.gradle because sometimes
+ // both :internal-testutils-paparazzi and :test:screenshot:screenshot are added to the
+ // classpath and picking a different package will cause missing class exceptions due to
+ // class shadowing
+ "androidx.test.screenshot.protobuf"
)
dependencies {
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt
index bfca2c7..71be049 100644
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt
+++ b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/GoldenVerifier.kt
@@ -110,8 +110,8 @@
)
is AnalysisResult.MissingGolden -> throw AssertionError(
"Expected golden image for test \"${snapshot.testName.methodName}\" does not " +
- "exist. Run ${updateGoldenGradleTask()} to create it and update all golden " +
- "images for this test module."
+ "exist. Run ${updateGoldenGradleTask()} -Pandroidx.ignoreTestFailures=true " +
+ "to create it and update all golden images for this test module."
)
}
}
diff --git a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt b/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt
index 6a460df..0775bf6 100644
--- a/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt
+++ b/testutils/testutils-paparazzi/src/test/kotlin/androidx/testutils/paparazzi/GoldenVerifierTest.kt
@@ -219,8 +219,8 @@
@Test
fun `asserts on missing golden`() {
val message = "Expected golden image for test \"asserts on missing golden\" does not " +
- "exist. Run :updateGolden to create it and update all golden images for this test " +
- "module."
+ "exist. Run :updateGolden -Pandroidx.ignoreTestFailures=true to create it and update " +
+ "all golden images for this test module."
assertFailsWithMessage(message) {
goldenVerifier().assertMatchesGolden(snapshot(), loadTestImage("circle"))
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 0cfac0c..0f056c9 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -45,6 +45,7 @@
androidTestImplementation(project(":wear:compose:compose-navigation-samples"))
androidTestImplementation(libs.truth)
androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.5.1")
+ androidTestImplementation("androidx.navigation:navigation-testing:2.5.3")
samples(project(":wear:compose:compose-navigation-samples"))
}
diff --git a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
index 4ad9e64..68f68b5 100644
--- a/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
+++ b/wear/compose/compose-navigation/src/androidTest/kotlin/androidx/wear/compose/navigation/SwipeDismissableNavHostTest.kt
@@ -34,6 +34,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -49,6 +50,7 @@
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
+import androidx.navigation.testing.TestNavHostController
import androidx.wear.compose.material.Button
import androidx.wear.compose.material.CompactChip
import androidx.wear.compose.material.MaterialTheme
@@ -390,6 +392,35 @@
}
}
+ @Test
+ fun testNavHostController_starts_at_default_destination() {
+ lateinit var navController: TestNavHostController
+
+ rule.setContentWithTheme {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(WearNavigator())
+
+ SwipeDismissWithNavigation(navController)
+ }
+
+ rule.onNodeWithText(START).assertExists()
+ }
+
+ @Test
+ fun testNavHostController_sets_current_destination() {
+ lateinit var navController: TestNavHostController
+
+ rule.setContentWithTheme {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(WearNavigator())
+
+ SwipeDismissWithNavigation(navController)
+ navController.setCurrentDestination(NEXT)
+ }
+
+ rule.onNodeWithText(NEXT).assertExists()
+ }
+
@Composable
fun SwipeDismissWithNavigation(
navController: NavHostController = rememberSwipeDismissableNavController()
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 32cca2b..2468ae7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -43,7 +43,9 @@
import androidx.wear.protolayout.expression.pipeline.FloatNodes.FixedFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.Int32ToFloatNode;
import androidx.wear.protolayout.expression.pipeline.FloatNodes.StateFloatNode;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.AnimatableFixedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.ArithmeticInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.DynamicAnimatedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FixedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FloatToInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.PlatformInt32SourceNode;
@@ -58,6 +60,7 @@
import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicColor;
import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalFloatOp;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalStringOp;
@@ -68,6 +71,7 @@
import androidx.wear.protolayout.expression.proto.DynamicProto.DynamicString;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
+import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
import java.util.ArrayList;
import java.util.List;
@@ -340,7 +344,8 @@
@NonNull DynamicBuilders.DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
- bindRecursively(int32Source.toDynamicInt32Proto(), consumer, resultBuilder);
+ bindRecursively(
+ int32Source.toDynamicInt32Proto(), consumer, resultBuilder, Optional.empty());
mUiHandler.post(() -> processBindings(resultBuilder));
return new BoundDynamicTypeImpl(resultBuilder);
}
@@ -365,7 +370,33 @@
@NonNull DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer) {
List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
- bindRecursively(int32Source, consumer, resultBuilder);
+ bindRecursively(int32Source, consumer, resultBuilder, Optional.empty());
+ mDynamicTypeNodes.addAll(resultBuilder);
+ return new BoundDynamicTypeImpl(resultBuilder);
+ }
+
+ /**
+ * Adds pending expression from the given {@link DynamicInt32} for future evaluation.
+ *
+ * <p>While the {@link BoundDynamicType} is not destroyed with{@link BoundDynamicType#close()}
+ * by caller, results of evaluation will be sent through the given {@link
+ * DynamicTypeValueReceiver}.
+ *
+ * @param int32Source The given integer dynamic type that should be evaluated.
+ * @param consumer The registered consumer for results of the evaluation. It will be called from
+ * UI thread.
+ * @param animationFallbackValue The value used if the given {@link DynamicInt32} is animatable
+ * and animations are disabled.
+ * @hide
+ */
+ @NonNull
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public BoundDynamicType bind(
+ @NonNull DynamicInt32 int32Source,
+ @NonNull DynamicTypeValueReceiver<Integer> consumer,
+ int animationFallbackValue) {
+ List<DynamicDataNode<?>> resultBuilder = new ArrayList<>();
+ bindRecursively(int32Source, consumer, resultBuilder, Optional.of(animationFallbackValue));
mDynamicTypeNodes.addAll(resultBuilder);
return new BoundDynamicTypeImpl(resultBuilder);
}
@@ -598,7 +629,8 @@
bindRecursively(
stringSource.getInt32FormatOp().getInput(),
int32FormatNode.getIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
break;
}
case FLOAT_FORMAT_OP:
@@ -677,7 +709,8 @@
private void bindRecursively(
@NonNull DynamicInt32 int32Source,
@NonNull DynamicTypeValueReceiver<Integer> consumer,
- @NonNull List<DynamicDataNode<?>> resultBuilder) {
+ @NonNull List<DynamicDataNode<?>> resultBuilder,
+ @NonNull Optional<Integer> animationFallbackValue) {
DynamicDataNode<Integer> node;
switch (int32Source.getInnerCase()) {
@@ -685,8 +718,7 @@
node = new FixedInt32Node(int32Source.getFixed(), consumer);
break;
case PLATFORM_SOURCE:
- node =
- new PlatformInt32SourceNode(
+ node = new PlatformInt32SourceNode(
int32Source.getPlatformSource(),
mTimeDataSource,
mSensorGatewayDataSource,
@@ -701,11 +733,13 @@
bindRecursively(
int32Source.getArithmeticOperation().getInputLhs(),
arithmeticNode.getLhsIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
bindRecursively(
int32Source.getArithmeticOperation().getInputRhs(),
arithmeticNode.getRhsIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
break;
}
@@ -728,11 +762,13 @@
bindRecursively(
op.getValueIfTrue(),
conditionalNode.getTrueValueIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
bindRecursively(
op.getValueIfFalse(),
conditionalNode.getFalseValueIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
node = conditionalNode;
break;
@@ -750,6 +786,53 @@
Optional.empty());
break;
}
+ case ANIMATABLE_FIXED:
+ if (!mEnableAnimations && animationFallbackValue.isPresent()) {
+ // Just assign static value if animations are disabled.
+ node =
+ new FixedInt32Node(
+ FixedInt32.newBuilder().setValue(
+ animationFallbackValue.get()).build(), consumer);
+
+ } else {
+ // We don't have to check if enableAnimations is true, because if it's false
+ // and we didn't
+ // have static value set, constructor has put QuotaManager that don't have
+ // any quota, so
+ // animations won't be played and they would jump to the end value.
+ node =
+ new AnimatableFixedInt32Node(
+ int32Source.getAnimatableFixed(), consumer,
+ mAnimationQuotaManager);
+ }
+ break;
+ case ANIMATABLE_DYNAMIC:
+ if (!mEnableAnimations && animationFallbackValue.isPresent()) {
+ // Just assign static value if animations are disabled.
+ node =
+ new FixedInt32Node(
+ FixedInt32.newBuilder().setValue(
+ animationFallbackValue.get()).build(), consumer);
+
+ } else {
+ // We don't have to check if enableAnimations is true, because if it's false
+ // and we didn't
+ // have static value set, constructor has put QuotaManager that don't have
+ // any quota, so
+ // animations won't be played and they would jump to the end value.
+ AnimatableDynamicInt32 dynamicNode = int32Source.getAnimatableDynamic();
+ DynamicAnimatedInt32Node animationNode =
+ new DynamicAnimatedInt32Node(consumer, dynamicNode.getSpec(),
+ mAnimationQuotaManager);
+ node = animationNode;
+
+ bindRecursively(
+ dynamicNode.getInput(),
+ animationNode.getInputCallback(),
+ resultBuilder,
+ animationFallbackValue);
+ }
+ break;
case INNER_NOT_SET:
throw new IllegalArgumentException("DynamicInt32 has no inner source set");
default:
@@ -807,7 +890,8 @@
bindRecursively(
floatSource.getInt32ToFloatOperation().getInput(),
toFloatNode.getIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
break;
}
case CONDITIONAL_OP:
@@ -999,11 +1083,13 @@
bindRecursively(
boolSource.getInt32Comparison().getInputLhs(),
compNode.getLhsIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
bindRecursively(
boolSource.getInt32Comparison().getInputRhs(),
compNode.getRhsIncomingCallback(),
- resultBuilder);
+ resultBuilder,
+ Optional.empty());
break;
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
index 53a7968..4fc75c6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -16,13 +16,19 @@
package androidx.wear.protolayout.expression.pipeline;
+import static androidx.wear.protolayout.expression.pipeline.AnimationsHelper.applyAnimationSpecToAnimator;
+
+import android.animation.ValueAnimator;
import android.util.Log;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.EpochTimePlatformDataSource;
import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.PlatformDataSource;
import androidx.wear.protolayout.expression.pipeline.PlatformDataSources.SensorGatewayPlatformDataSource;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.ArithmeticInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.FloatToInt32Op;
import androidx.wear.protolayout.expression.proto.DynamicProto.PlatformInt32Source;
@@ -203,4 +209,125 @@
});
}
}
+
+ /** Dynamic int32 node that gets animatable value from fixed source. */
+ static class AnimatableFixedInt32Node extends AnimatableNode
+ implements DynamicDataSourceNode<Integer> {
+
+ private final AnimatableFixedInt32 mProtoNode;
+ private final DynamicTypeValueReceiver<Integer> mDownstream;
+
+ AnimatableFixedInt32Node(
+ AnimatableFixedInt32 protoNode,
+ DynamicTypeValueReceiver<Integer> downstream,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mProtoNode = protoNode;
+ this.mDownstream = downstream;
+ }
+
+ @Override
+ @UiThread
+ public void preInit() {
+ mDownstream.onPreUpdate();
+ }
+
+ @Override
+ @UiThread
+ public void init() {
+ ValueAnimator animator =
+ ValueAnimator.ofInt(mProtoNode.getFromValue(), mProtoNode.getToValue());
+ applyAnimationSpecToAnimator(animator, mProtoNode.getSpec());
+ animator.addUpdateListener(a -> mDownstream.onData((Integer) a.getAnimatedValue()));
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+
+ @Override
+ @UiThread
+ public void destroy() {
+ mQuotaAwareAnimator.stopAnimator();
+ }
+ }
+
+ /** Dynamic int32 node that gets animatable value from dynamic source. */
+ static class DynamicAnimatedInt32Node extends AnimatableNode implements
+ DynamicDataNode<Integer> {
+
+ final DynamicTypeValueReceiver<Integer> mDownstream;
+ private final DynamicTypeValueReceiver<Integer> mInputCallback;
+
+ @Nullable
+ Integer mCurrentValue = null;
+ int mPendingCalls = 0;
+
+ // Static analysis complains about calling methods of parent class AnimatableNode under
+ // initialization but mInputCallback is only used after the constructor is finished.
+ @SuppressWarnings("method.invocation.invalid")
+ DynamicAnimatedInt32Node(
+ DynamicTypeValueReceiver<Integer> downstream,
+ @NonNull AnimationSpec spec,
+ QuotaManager quotaManager) {
+ super(quotaManager);
+ this.mDownstream = downstream;
+ this.mInputCallback =
+ new DynamicTypeValueReceiver<Integer>() {
+ @Override
+ public void onPreUpdate() {
+ mPendingCalls++;
+
+ if (mPendingCalls == 1) {
+ mDownstream.onPreUpdate();
+
+ mQuotaAwareAnimator.resetAnimator();
+ }
+ }
+
+ @Override
+ public void onData(Integer newData) {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ if (mCurrentValue == null) {
+ mCurrentValue = newData;
+ mDownstream.onData(mCurrentValue);
+ } else {
+ ValueAnimator animator = ValueAnimator.ofInt(mCurrentValue,
+ newData);
+
+ applyAnimationSpecToAnimator(animator, spec);
+ animator.addUpdateListener(
+ a -> {
+ if (mPendingCalls == 0) {
+ mCurrentValue = (Integer) a.getAnimatedValue();
+ mDownstream.onData(mCurrentValue);
+ }
+ });
+
+ mQuotaAwareAnimator.updateAnimator(animator);
+ startOrSkipAnimator();
+ }
+ }
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (mPendingCalls > 0) {
+ mPendingCalls--;
+ }
+
+ if (mPendingCalls == 0) {
+ mCurrentValue = null;
+ mDownstream.onInvalidated();
+ }
+ }
+ };
+ }
+
+ public DynamicTypeValueReceiver<Integer> getInputCallback() {
+ return mInputCallback;
+ }
+ }
}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
index dc0d8d4..ddb9ec7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/Int32NodesTest.java
@@ -18,14 +18,23 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.os.Looper;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.AnimatableFixedInt32Node;
+import androidx.wear.protolayout.expression.pipeline.Int32Nodes.DynamicAnimatedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.FixedInt32Node;
import androidx.wear.protolayout.expression.pipeline.Int32Nodes.StateInt32SourceNode;
+import androidx.wear.protolayout.expression.proto.AnimationParameterProto.AnimationSpec;
+import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableFixedInt32;
import androidx.wear.protolayout.expression.proto.DynamicProto.StateInt32Source;
import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
import androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue;
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -97,4 +106,127 @@
assertThat(results).containsExactly(12);
}
+
+ @Test
+ public void animatableFixedInt32_animates() {
+ int startValue = 3;
+ int endValue = 33;
+ List<Integer> results = new ArrayList<>();
+ QuotaManager quotaManager = new UnlimitedQuotaManager();
+ AnimatableFixedInt32 protoNode =
+ AnimatableFixedInt32.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedInt32Node node =
+ new AnimatableFixedInt32Node(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(true);
+
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results.size()).isGreaterThan(2);
+ assertThat(results.get(0)).isEqualTo(startValue);
+ assertThat(Iterables.getLast(results)).isEqualTo(endValue);
+ }
+
+ @Test
+ public void animatableFixedInt32_whenInvisible_skipToEnd() {
+ int startValue = 3;
+ int endValue = 33;
+ List<Integer> results = new ArrayList<>();
+ QuotaManager quotaManager = new UnlimitedQuotaManager();
+ AnimatableFixedInt32 protoNode =
+ AnimatableFixedInt32.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedInt32Node node =
+ new AnimatableFixedInt32Node(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(false);
+
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(endValue);
+ }
+
+ @Test
+ public void animatableFixedInt32_whenNoQuota_skip() {
+ int startValue = 3;
+ int endValue = 33;
+ List<Integer> results = new ArrayList<>();
+ QuotaManager quotaManager = new TestNoQuotaManagerImpl();
+ AnimatableFixedInt32 protoNode =
+ AnimatableFixedInt32.newBuilder().setFromValue(startValue).setToValue(
+ endValue).build();
+ AnimatableFixedInt32Node node =
+ new AnimatableFixedInt32Node(protoNode, new AddToListCallback<>(results),
+ quotaManager);
+ node.setVisibility(true);
+
+ node.init();
+ shadowOf(Looper.getMainLooper()).idle();
+
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(endValue);
+ }
+
+ @Test
+ public void dynamicAnimatedInt32_onlyAnimateWhenVisible() {
+ int value1 = 3;
+ int value2 = 11;
+ int value3 = 17;
+ List<Integer> results = new ArrayList<>();
+ QuotaManager quotaManager = new UnlimitedQuotaManager();
+ ObservableStateStore oss =
+ new ObservableStateStore(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setInt32Val(
+ FixedInt32.newBuilder().setValue(value1).build())
+ .build()));
+ DynamicAnimatedInt32Node int32Node =
+ new DynamicAnimatedInt32Node(
+ new AddToListCallback<>(results), AnimationSpec.getDefaultInstance(),
+ quotaManager);
+ int32Node.setVisibility(false);
+ StateInt32SourceNode stateNode =
+ new StateInt32SourceNode(
+ oss,
+ StateInt32Source.newBuilder().setSourceKey("foo").build(),
+ int32Node.getInputCallback());
+
+ stateNode.preInit();
+ stateNode.init();
+
+ results.clear();
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setInt32Val(FixedInt32.newBuilder().setValue(value2))
+ .build()));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Only contains last value.
+ assertThat(results).hasSize(1);
+ assertThat(results).containsExactly(value2);
+
+ int32Node.setVisibility(true);
+ results.clear();
+ oss.setStateEntryValuesProto(
+ ImmutableMap.of(
+ "foo",
+ StateEntryValue.newBuilder()
+ .setInt32Val(FixedInt32.newBuilder().setValue(value3))
+ .build()));
+ shadowOf(Looper.getMainLooper()).idle();
+
+ // Contains intermediate values besides the initial and last.
+ assertThat(results.size()).isGreaterThan(2);
+ assertThat(results.get(0)).isEqualTo(value2);
+ assertThat(Iterables.getLast(results)).isEqualTo(value3);
+ assertThat(results).isInOrder();
+ }
}
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index d9949ab..49d68e38 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -157,6 +157,12 @@
}
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -228,5 +234,20 @@
method public static androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue fromString(String);
}
+ public final class VersionBuilders {
+ }
+
+ public static final class VersionBuilders.VersionInfo {
+ method public int getMajor();
+ method public int getMinor();
+ }
+
+ public static final class VersionBuilders.VersionInfo.Builder {
+ ctor public VersionBuilders.VersionInfo.Builder();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo build();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMajor(int);
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMinor(int);
+ }
+
}
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index be278c8..5ab6ba0 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -157,6 +157,12 @@
}
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -231,5 +237,20 @@
method public static androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue fromString(String);
}
+ public final class VersionBuilders {
+ }
+
+ public static final class VersionBuilders.VersionInfo {
+ method public int getMajor();
+ method public int getMinor();
+ }
+
+ public static final class VersionBuilders.VersionInfo.Builder {
+ ctor public VersionBuilders.VersionInfo.Builder();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo build();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMajor(int);
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMinor(int);
+ }
+
}
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index d9949ab..49d68e38 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -157,6 +157,12 @@
}
public static interface DynamicBuilders.DynamicInt32 extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(int, int, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String);
+ method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(String, androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec);
+ method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 animate();
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat asFloat();
method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 constant(int);
method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32 div(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32);
@@ -228,5 +234,20 @@
method public static androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue fromString(String);
}
+ public final class VersionBuilders {
+ }
+
+ public static final class VersionBuilders.VersionInfo {
+ method public int getMajor();
+ method public int getMinor();
+ }
+
+ public static final class VersionBuilders.VersionInfo.Builder {
+ ctor public VersionBuilders.VersionInfo.Builder();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo build();
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMajor(int);
+ method public androidx.wear.protolayout.expression.VersionBuilders.VersionInfo.Builder setMinor(int);
+ }
+
}
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index dfecd528..110f68a 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -884,6 +884,302 @@
}
/**
+ * A static interpolation node, between two fixed int32 values.
+ *
+ * @since 1.2
+ */
+ static final class AnimatableFixedInt32 implements DynamicInt32 {
+ private final DynamicProto.AnimatableFixedInt32 mImpl;
+ @Nullable private final Fingerprint mFingerprint;
+
+ AnimatableFixedInt32(
+ DynamicProto.AnimatableFixedInt32 impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the value to start animating from.
+ *
+ * @since 1.2
+ */
+ public int getFromValue() {
+ return mImpl.getFromValue();
+ }
+
+ /**
+ * Gets the value to animate to.
+ *
+ * @since 1.2
+ */
+ public int getToValue() {
+ return mImpl.getToValue();
+ }
+
+ /**
+ * Gets the animation parameters for duration, delay, etc.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public AnimationSpec getSpec() {
+ if (mImpl.hasSpec()) {
+ return AnimationSpec.fromProto(mImpl.getSpec());
+ } else {
+ return null;
+ }
+ }
+
+ /** @hide */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+ /**
+ * Creates a new wrapper instance from the proto.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static AnimatableFixedInt32 fromProto(
+ @NonNull DynamicProto.AnimatableFixedInt32 proto, @Nullable Fingerprint fingerprint) {
+ return new AnimatableFixedInt32(proto, fingerprint);
+ }
+
+ @NonNull
+ static AnimatableFixedInt32 fromProto(@NonNull DynamicProto.AnimatableFixedInt32 proto) {
+ return fromProto(proto, null);
+ }
+
+ /**
+ * Returns the internal proto instance.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ DynamicProto.AnimatableFixedInt32 toProto() {
+ return mImpl;
+ }
+
+ /** @hide */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+ return DynamicProto.DynamicInt32.newBuilder().setAnimatableFixed(mImpl).build();
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "AnimatableFixedInt32{"
+ + "fromValue="
+ + getFromValue()
+ + ", toValue="
+ + getToValue()
+ + ", spec="
+ + getSpec()
+ + "}";
+ }
+
+ /** Builder for {@link AnimatableFixedInt32}. */
+ public static final class Builder implements DynamicInt32.Builder {
+ private final DynamicProto.AnimatableFixedInt32.Builder mImpl =
+ DynamicProto.AnimatableFixedInt32.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1831435966);
+
+ public Builder() {}
+
+ /**
+ * Sets the value to start animating from.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public AnimatableFixedInt32.Builder setFromValue(int fromValue) {
+ mImpl.setFromValue(fromValue);
+ mFingerprint.recordPropertyUpdate(1, fromValue);
+ return this;
+ }
+
+ /**
+ * Sets the value to animate to.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public AnimatableFixedInt32.Builder setToValue(int toValue) {
+ mImpl.setToValue(toValue);
+ mFingerprint.recordPropertyUpdate(2, toValue);
+ return this;
+ }
+
+ /**
+ * Sets the animation parameters for duration, delay, etc.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public AnimatableFixedInt32.Builder setSpec(@NonNull AnimationSpec spec) {
+ mImpl.setSpec(spec.toProto());
+ mFingerprint.recordPropertyUpdate(
+ 3, checkNotNull(spec.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public AnimatableFixedInt32 build() {
+ return new AnimatableFixedInt32(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
+ * A dynamic interpolation node. This will watch the value of its input and, when the first update
+ * arrives, immediately emit that value. On subsequent updates, it will animate between the old
+ * and new values.
+ *
+ * <p>If this node receives an invalid value (e.g. as a result of an upstream node having no
+ * value), then it will emit a single invalid value, and forget its "stored" value. The next valid
+ * value that arrives is then used as the "first" value again.
+ *
+ * @since 1.2
+ */
+ static final class AnimatableDynamicInt32 implements DynamicInt32 {
+ private final DynamicProto.AnimatableDynamicInt32 mImpl;
+ @Nullable private final Fingerprint mFingerprint;
+
+ AnimatableDynamicInt32(
+ DynamicProto.AnimatableDynamicInt32 impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets the value to watch, and animate when it changes.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public DynamicInt32 getInput() {
+ if (mImpl.hasInput()) {
+ return dynamicInt32FromProto(mImpl.getInput());
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the animation parameters for duration, delay, etc.
+ *
+ * @since 1.2
+ */
+ @Nullable
+ public AnimationSpec getSpec() {
+ if (mImpl.hasSpec()) {
+ return AnimationSpec.fromProto(mImpl.getSpec());
+ } else {
+ return null;
+ }
+ }
+
+ /** @hide */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+ /**
+ * Creates a new wrapper instance from the proto.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static AnimatableDynamicInt32 fromProto(
+ @NonNull DynamicProto.AnimatableDynamicInt32 proto, @Nullable Fingerprint fingerprint) {
+ return new AnimatableDynamicInt32(proto, fingerprint);
+ }
+
+ @NonNull
+ static AnimatableDynamicInt32 fromProto(@NonNull DynamicProto.AnimatableDynamicInt32 proto) {
+ return fromProto(proto, null);
+ }
+
+ /**
+ * Returns the internal proto instance.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ DynamicProto.AnimatableDynamicInt32 toProto() {
+ return mImpl;
+ }
+
+ /** @hide */
+ @Override
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public DynamicProto.DynamicInt32 toDynamicInt32Proto() {
+ return DynamicProto.DynamicInt32.newBuilder().setAnimatableDynamic(mImpl).build();
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "AnimatableDynamicInt32{" + "input=" + getInput() + ", spec=" + getSpec() + "}";
+ }
+
+ /** Builder for {@link AnimatableDynamicInt32}. */
+ public static final class Builder implements DynamicInt32.Builder {
+ private final DynamicProto.AnimatableDynamicInt32.Builder mImpl =
+ DynamicProto.AnimatableDynamicInt32.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(-1554674954);
+
+ public Builder() {}
+
+ /**
+ * Sets the value to watch, and animate when it changes.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public AnimatableDynamicInt32.Builder setInput(@NonNull DynamicInt32 input) {
+ mImpl.setInput(input.toDynamicInt32Proto());
+ mFingerprint.recordPropertyUpdate(
+ 1, checkNotNull(input.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ /**
+ * Sets the animation parameters for duration, delay, etc.
+ *
+ * @since 1.2
+ */
+ @NonNull
+ public AnimatableDynamicInt32.Builder setSpec(@NonNull AnimationSpec spec) {
+ mImpl.setSpec(spec.toProto());
+ mFingerprint.recordPropertyUpdate(
+ 2, checkNotNull(spec.getFingerprint()).aggregateValueAsInt());
+ return this;
+ }
+
+ @Override
+ @NonNull
+ public AnimatableDynamicInt32 build() {
+ return new AnimatableDynamicInt32(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+
+ /**
* Interface defining a dynamic int32 type.
*
* <p>It offers a set of helper methods for creating arithmetic and logical expressions, e.g.
@@ -956,6 +1252,84 @@
return new StateInt32Source.Builder().setSourceKey(stateKey).build();
}
+ /**
+ * Creates a {@link DynamicInt32} which will animate from {@code start} to {@code end}.
+ *
+ * @param start The start value of the range.
+ * @param end The end value of the range.
+ */
+ @NonNull
+ static DynamicInt32 animate(int start, int end) {
+ return new AnimatableFixedInt32.Builder().setFromValue(start).setToValue(end).build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} which will animate from {@code start} to {@code end} with the
+ * given animation parameters.
+ *
+ * @param start The start value of the range.
+ * @param end The end value of the range.
+ * @param spec The animation parameters.
+ */
+ @NonNull
+ static DynamicInt32 animate(int start, int end, @NonNull AnimationSpec spec) {
+ return new AnimatableFixedInt32.Builder()
+ .setFromValue(start)
+ .setToValue(end)
+ .setSpec(spec)
+ .build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} that is bound to the value of an item of the State. Every time
+ * the state value changes, this {@link DynamicInt32} will animate from its current value to the
+ * new value (from the state).
+ *
+ * @param stateKey The key to a {@link StateEntryValue} with an int value from the provider's
+ * state.
+ */
+ @NonNull
+ static DynamicInt32 animate(@NonNull String stateKey) {
+ return new AnimatableDynamicInt32.Builder().setInput(fromState(stateKey)).build();
+ }
+
+ /**
+ * Creates a {@link DynamicInt32} that is bound to the value of an item of the State. Every time
+ * the state value changes, this {@link DynamicInt32} will animate from its current value to the
+ * new value (from the state).
+ *
+ * @param stateKey The key to a {@link StateEntryValue} with an int value from the provider's
+ * state.
+ * @param spec The animation parameters.
+ */
+ @NonNull
+ static DynamicInt32 animate(@NonNull String stateKey, @NonNull AnimationSpec spec) {
+ return new AnimatableDynamicInt32.Builder()
+ .setInput(fromState(stateKey))
+ .setSpec(spec)
+ .build();
+ }
+
+ /**
+ * Returns a {@link DynamicInt32} that is bound to the value of this {@link DynamicInt32} and
+ * every time its value is changing, it animates from its current value to the new value.
+ *
+ * @param spec The animation parameters.
+ */
+ @NonNull
+ default DynamicInt32 animate(@NonNull AnimationSpec spec) {
+ return new AnimatableDynamicInt32.Builder().setInput(this).setSpec(spec).build();
+ }
+
+ /**
+ * Returns a {@link DynamicInt32} that is bound to the value of this {@link DynamicInt32} and
+ * every time its value is changing, it animates from its current value to the new value.
+ */
+ @NonNull
+ default DynamicInt32 animate() {
+ return new AnimatableDynamicInt32.Builder().setInput(this).build();
+ }
+
/** Convert the value represented by this {@link DynamicInt32} into a {@link DynamicFloat}. */
@NonNull
default DynamicFloat asFloat() {
@@ -1741,6 +2115,12 @@
if (proto.hasFloatToInt()) {
return FloatToInt32Op.fromProto(proto.getFloatToInt());
}
+ if (proto.hasAnimatableFixed()) {
+ return AnimatableFixedInt32.fromProto(proto.getAnimatableFixed());
+ }
+ if (proto.hasAnimatableDynamic()) {
+ return AnimatableDynamicInt32.fromProto(proto.getAnimatableDynamic());
+ }
throw new IllegalStateException("Proto was not a recognised instance of DynamicInt32");
}
@@ -2818,7 +3198,7 @@
}
/**
- * An operation to convert a Int32 value in the dynamic data pipeline to a Float value.
+ * An operation to convert an Int32 value in the dynamic data pipeline to a Float value.
*
* @since 1.2
*/
@@ -2907,7 +3287,7 @@
}
/**
- * A static interpolation, between two fixed floating point values.
+ * A static interpolation node, between two fixed floating point values.
*
* @since 1.2
*/
@@ -4866,7 +5246,7 @@
}
/**
- * A static interpolation, between two fixed color values.
+ * A static interpolation node, between two fixed color values.
*
* @since 1.2
*/
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java
new file mode 100644
index 0000000..366e97c
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/VersionBuilders.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023 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.wear.protolayout.expression;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.proto.VersionProto;
+
+/** Builders for the schema version information of a layout (or an expression). */
+public final class VersionBuilders {
+ private VersionBuilders() {}
+
+ /**
+ * Version information. This is used to encode the schema version of a payload (e.g. inside of a
+ * layout).
+ *
+ * @since 1.0
+ */
+ public static final class VersionInfo {
+ private final VersionProto.VersionInfo mImpl;
+ @Nullable private final Fingerprint mFingerprint;
+
+ VersionInfo(VersionProto.VersionInfo impl, @Nullable Fingerprint fingerprint) {
+ this.mImpl = impl;
+ this.mFingerprint = fingerprint;
+ }
+
+ /**
+ * Gets major version. Incremented on breaking changes (i.e. compatibility is not guaranteed
+ * across major versions).
+ *
+ * @since 1.0
+ */
+ public int getMajor() {
+ return mImpl.getMajor();
+ }
+
+ /**
+ * Gets minor version. Incremented on non-breaking changes (e.g. schema additions). Anything
+ * consuming a payload can safely consume anything with a lower minor version.
+ *
+ * @since 1.0
+ */
+ public int getMinor() {
+ return mImpl.getMinor();
+ }
+
+ /**
+ * Get the fingerprint for this object, or null if unknown.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @Nullable
+ public Fingerprint getFingerprint() {
+ return mFingerprint;
+ }
+ /**
+ * Creates a new wrapper instance from the proto.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static VersionInfo fromProto(
+ @NonNull VersionProto.VersionInfo proto, @Nullable Fingerprint fingerprint) {
+ return new VersionInfo(proto, fingerprint);
+ }
+
+ /**
+ * Creates a new wrapper instance from the proto. Intended for testing purposes only. An object
+ * created using this method can't be added to any other wrapper.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public static VersionInfo fromProto(@NonNull VersionProto.VersionInfo proto) {
+ return fromProto(proto, null);
+ }
+
+ /**
+ * Returns the internal proto instance.
+ *
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
+ public VersionProto.VersionInfo toProto() {
+ return mImpl;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "VersionInfo{" + "major=" + getMajor() + ", minor=" + getMinor() + "}";
+ }
+
+ /** Builder for {@link VersionInfo} */
+ public static final class Builder {
+ private final VersionProto.VersionInfo.Builder mImpl = VersionProto.VersionInfo.newBuilder();
+ private final Fingerprint mFingerprint = new Fingerprint(77091996);
+
+ public Builder() {}
+
+ /**
+ * Sets major version. Incremented on breaking changes (i.e. compatibility is not guaranteed
+ * across major versions).
+ *
+ * @since 1.0
+ */
+ @NonNull
+ public Builder setMajor(int major) {
+ mImpl.setMajor(major);
+ mFingerprint.recordPropertyUpdate(1, major);
+ return this;
+ }
+
+ /**
+ * Sets minor version. Incremented on non-breaking changes (e.g. schema additions). Anything
+ * consuming a payload can safely consume anything with a lower minor version.
+ *
+ * @since 1.0
+ */
+ @NonNull
+ public Builder setMinor(int minor) {
+ mImpl.setMinor(minor);
+ mFingerprint.recordPropertyUpdate(2, minor);
+ return this;
+ }
+
+ /** Builds an instance from accumulated values. */
+ @NonNull
+ public VersionInfo build() {
+ return new VersionInfo(mImpl.build(), mFingerprint);
+ }
+ }
+ }
+}
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java
new file mode 100644
index 0000000..f8e4c67
--- /dev/null
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/VersionInfoTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.wear.protolayout.expression;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public final class VersionInfoTest {
+ @Test
+ public void versionInfo() {
+ int major = 10;
+ int minor = 20;
+
+ VersionBuilders.VersionInfo versionInfo =
+ new VersionBuilders.VersionInfo.Builder().setMajor(major).setMinor(minor).build();
+
+
+ assertThat(versionInfo.toProto().getMajor()).isEqualTo(major);
+ assertThat(versionInfo.toProto().getMinor()).isEqualTo(minor);
+ }
+
+}
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
index 25c9e37..bcd3063 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/device_parameters.proto
@@ -54,7 +54,7 @@
ScreenShape screen_shape = 5;
// The maximum schema version supported by the current renderer.
- VersionInfo renderer_schema_version = 6;
+ androidx.wear.protolayout.expression.proto.VersionInfo renderer_schema_version = 6;
// Renderer supported capabilities
Capabilities capabilities = 8;
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
index adc7d78..52dfa35 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
@@ -132,7 +132,55 @@
FloatToInt32RoundMode round_mode = 2;
}
+// A static interpolation node, between two fixed int32 values.
+message AnimatableFixedInt32 {
+ // The value to start animating from.
+ int32 from_value = 1;
+
+ // The value to animate to.
+ int32 to_value = 2;
+
+ // The animation parameters for duration, delay, etc.
+ AnimationSpec spec = 3;
+}
+
+// A dynamic interpolation node. This will watch the value of its input and,
+// when the first update arrives, immediately emit that value. On subsequent
+// updates, it will animate between the old and new values.
+//
+// If this node receives an invalid value (e.g. as a result of an upstream node
+// having no value), then it will emit a single invalid value, and forget its
+// "stored" value. The next valid value that arrives is then used as the
+// "first" value again.
+message AnimatableDynamicInt32 {
+ // The value to watch, and animate when it changes.
+ DynamicInt32 input = 1;
+
+ // The animation parameters for duration, delay, etc.
+ AnimationSpec spec = 2;
+}
+
// A dynamic int32 type.
+//
+// It offers a set of helper methods for creating arithmetic and logical
+// expressions, e.g. {@link #plus(int)}, {@link #times(int)}, {@link #eq(int)},
+// etc. These helper methods produce expression trees based on the order in
+// which they were called in an expression. Thus, no operator precedence rules
+// are applied.
+//
+// <p>For example the following expression is equivalent to {@code result = ((a
+// + b)*c)/d }:
+//
+// ```
+// a.plus(b).times(c).div(d);
+// ```
+//
+// More complex expressions can be created by nesting expressions. For example
+// the following expression is equivalent to {@code result = (a + b)*(c - d) }:
+//
+// ```
+// (a.plus(b)).times(c.minus(d));
+// ```
message DynamicInt32 {
oneof inner {
FixedInt32 fixed = 1;
@@ -141,6 +189,8 @@
StateInt32Source state_source = 4;
ConditionalInt32Op conditional_op = 5;
FloatToInt32Op float_to_int = 6;
+ AnimatableFixedInt32 animatable_fixed = 8;
+ AnimatableDynamicInt32 animatable_dynamic = 9;
}
}
@@ -260,7 +310,7 @@
DynamicInt32 input = 1;
}
-// A static interpolation, between two fixed floating point values.
+// A static interpolation node, between two fixed floating point values.
message AnimatableFixedFloat {
// The number to start animating from.
float from_value = 1;
@@ -289,6 +339,26 @@
}
// A dynamic float type.
+//
+// It offers a set of helper methods for creating arithmetic and logical
+// expressions, e.g. {@link #plus(float)}, {@link #times(float)}, {@link
+// #eq(float)}, etc. These helper methods produce expression trees based on the
+// order in which they were called in an expression. Thus, no operator
+// precedence rules are applied.
+//
+// <p>For example the following expression is equivalent to {@code result = ((a
+// + b)*c)/d }:
+//
+// ```
+// a.plus(b).times(c).div(d);
+// ```
+//
+// More complex expressions can be created by nesting expressions. For example
+// the following expression is equivalent to {@code result = (a + b)*(c - d) }:
+//
+// ```
+// (a.plus(b)).times(c.minus(d));
+// ```
message DynamicFloat {
oneof inner {
FixedFloat fixed = 1;
@@ -411,7 +481,7 @@
string source_key = 1;
}
-// A static interpolation, between two fixed color values.
+// A static interpolation node, between two fixed color values.
message AnimatableFixedColor {
// The color value (in ARGB format) to start animating from.
uint32 from_argb = 1;
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/version.proto b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
index 7c581e3..636de02 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/version.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/version.proto
@@ -1,9 +1,9 @@
// The schema version information of a layout.
syntax = "proto3";
-package androidx.wear.protolayout.proto;
+package androidx.wear.protolayout.expression.proto;
-option java_package = "androidx.wear.protolayout.proto";
+option java_package = "androidx.wear.protolayout.expression.proto";
option java_outer_classname = "VersionProto";
// Version information. This is used to encode the schema version of a payload
diff --git a/wear/tiles/tiles-proto/src/main/proto/tile.proto b/wear/tiles/tiles-proto/src/main/proto/tile.proto
index fc3d294..4f2b1b6 100644
--- a/wear/tiles/tiles-proto/src/main/proto/tile.proto
+++ b/wear/tiles/tiles-proto/src/main/proto/tile.proto
@@ -22,7 +22,7 @@
androidx.wear.protolayout.proto.Timeline timeline = 2;
// The schema version that this tile was built with.
- androidx.wear.protolayout.proto.VersionInfo schema_version = 3;
+ androidx.wear.protolayout.expression.proto.VersionInfo schema_version = 3;
// How many milliseconds of elapsed time (**not** wall clock time) this tile
// can be considered to be "fresh". The platform will attempt to refresh
diff --git a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
index dc56dd4..1ad1937 100644
--- a/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
+++ b/wear/tiles/tiles/src/main/java/androidx/wear/tiles/TileBuilders.java
@@ -22,7 +22,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.wear.tiles.TimelineBuilders.Timeline;
import androidx.wear.tiles.proto.TileProto;
-import androidx.wear.protolayout.proto.VersionProto.VersionInfo;
+import androidx.wear.protolayout.expression.proto.VersionProto.VersionInfo;
/** Builders for the components of a tile that can be rendered by a tile renderer. */
public final class TileBuilders {
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 44aa387..ae7dd7e 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -50,20 +50,25 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
val unevaluatedData: WireComplicationData,
- private val sensorGateway: SensorGateway? = null,
private val stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
+ private val sensorGateway: SensorGateway? = null,
) : AutoCloseable {
/**
* Java compatibility class for [ComplicationDataExpressionEvaluator].
*
* Unlike [data], [listener] is not invoked until there is a value (until [data] is non-null).
*/
- class Compat(
+ class Compat
+ @JvmOverloads
+ constructor(
val unevaluatedData: WireComplicationData,
private val executor: Executor,
private val listener: Consumer<WireComplicationData>,
+ stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
+ sensorGateway: SensorGateway? = null,
) : AutoCloseable {
- private val evaluator = ComplicationDataExpressionEvaluator(unevaluatedData)
+ private val evaluator =
+ ComplicationDataExpressionEvaluator(unevaluatedData, stateStore, sensorGateway)
/** @see ComplicationDataExpressionEvaluator.init */
fun init() {
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 37cae45..47e9331 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -262,32 +262,28 @@
// Defensive copy due to in-place evaluation.
val expressed = WireComplicationData.Builder(scenario.expressed).build()
val stateStore = ObservableStateStore(mapOf())
- ComplicationDataExpressionEvaluator(
- expressed,
- stateStore = stateStore,
- )
- .use { evaluator ->
- val allEvaluations =
- evaluator.data
- .filterNotNull()
- .shareIn(
- CoroutineScope(Dispatchers.Main),
- SharingStarted.Eagerly,
- replay = 10,
- )
- evaluator.init()
- runUiThreadTasks() // Ensures data sharing started.
+ ComplicationDataExpressionEvaluator(expressed, stateStore).use { evaluator ->
+ val allEvaluations =
+ evaluator.data
+ .filterNotNull()
+ .shareIn(
+ CoroutineScope(Dispatchers.Main),
+ SharingStarted.Eagerly,
+ replay = 10,
+ )
+ evaluator.init()
+ runUiThreadTasks() // Ensures data sharing started.
- for (state in scenario.states) {
- stateStore.setStateEntryValues(state)
- runUiThreadTasks() // Ensures data sharing ended.
- }
-
- expect
- .withMessage(scenario.name)
- .that(allEvaluations.replayCache)
- .isEqualTo(scenario.evaluated)
+ for (state in scenario.states) {
+ stateStore.setStateEntryValues(state)
+ runUiThreadTasks() // Ensures data sharing ended.
}
+
+ expect
+ .withMessage(scenario.name)
+ .that(allEvaluations.replayCache)
+ .isEqualTo(scenario.evaluated)
+ }
}
}
diff --git a/webkit/integration-tests/testapp/build.gradle b/webkit/integration-tests/testapp/build.gradle
index c7f39ec..2d2e298 100644
--- a/webkit/integration-tests/testapp/build.gradle
+++ b/webkit/integration-tests/testapp/build.gradle
@@ -31,7 +31,6 @@
implementation(libs.espressoIdlingNet)
implementation(libs.espressoIdlingResource)
- androidTestImplementation(libs.espressoRemote)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.testRunner)
diff --git a/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
index 5ec74fb..99cbcac 100644
--- a/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
+++ b/webkit/integration-tests/testapp/src/androidTest/AndroidManifest.xml
@@ -15,13 +15,4 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
- <instrumentation
- android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.example.androidx.webkit"
- android:targetProcesses="*">
- <!-- This enables Multiprocess Espresso. -->
- <meta-data
- android:name="remoteMethod"
- android:value="androidx.test.espresso.remote.EspressoRemote#remoteInit" />
- </instrumentation>
</manifest>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
index 43e3a0d..8babd32 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -16,11 +16,6 @@
package com.example.androidx.webkit;
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-import static androidx.test.espresso.matcher.ViewMatchers.withText;
-
import static org.junit.Assert.assertTrue;
import androidx.core.content.ContextCompat;
@@ -44,8 +39,8 @@
@LargeTest
public class ProcessGlobalConfigActivityTestAppTest {
@Rule
- public ActivityScenarioRule<MainActivity> mRule =
- new ActivityScenarioRule<>(MainActivity.class);
+ public ActivityScenarioRule<ProcessGlobalConfigActivity> mRule =
+ new ActivityScenarioRule<>(ProcessGlobalConfigActivity.class);
@Before
public void setUp() {
@@ -70,8 +65,9 @@
+ "delete it");
}
WebkitTestHelpers.clickMenuListItemWithString(
- R.string.process_global_config_activity_title);
- onView(withId(R.id.process_global_textview)).check(matches(withText("WebView Loaded!")));
+ R.string.data_directory_suffix_activity_title);
+ // We need to wait for the WebView to finish loading on a different process.
+ Thread.sleep(5000);
assertTrue(file.exists());
}
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProxyOverrideTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProxyOverrideTestAppTest.java
index 18a126c..45deec8 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProxyOverrideTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProxyOverrideTestAppTest.java
@@ -22,7 +22,6 @@
import androidx.webkit.WebViewFeature;
import org.junit.Before;
-import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -42,7 +41,6 @@
WebkitTestHelpers.assumeFeature(WebViewFeature.PROXY_OVERRIDE);
}
- @Ignore // b/267381688
@Test
public void testProxyOverride() {
WebkitTestHelpers.clickMenuListItemWithString(R.string.proxy_override_activity_title);
@@ -58,7 +56,6 @@
R.string.proxy_override_requests_served, 1);
}
- @Ignore // b/267381688
@Test
public void testReverseBypass() {
WebkitTestHelpers.assumeFeature(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS);
diff --git a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
index 0428b05..fd36cc4 100644
--- a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -125,7 +125,10 @@
android:exported="true" />
<activity
android:name=".ProcessGlobalConfigActivity"
- android:process=":processGlobalConfigTest"
+ android:exported="true" />
+ <activity
+ android:name=".DataDirectorySuffixActivity"
+ android:process=":dataDirectorySuffixActivity"
android:exported="true" />
<activity
android:name=".RequestedWithHeaderActivity"
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DataDirectorySuffixActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DataDirectorySuffixActivity.java
new file mode 100644
index 0000000..1c92b41
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/DataDirectorySuffixActivity.java
@@ -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 com.example.androidx.webkit;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.webkit.ProcessGlobalConfig;
+import androidx.webkit.WebViewFeature;
+
+
+/**
+ * An {@link Activity} which makes use of
+ * {@link androidx.webkit.ProcessGlobalConfig#setDataDirectorySuffix(Context, String)}.
+ */
+public class DataDirectorySuffixActivity extends AppCompatActivity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTitle(R.string.data_directory_suffix_activity_title);
+ WebkitHelpers.appendWebViewVersionToTitle(this);
+
+ if (!WebViewFeature.isStartupFeatureSupported(this,
+ WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) {
+ WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
+ return;
+ }
+ ProcessGlobalConfig config = new ProcessGlobalConfig();
+ config.setDataDirectorySuffix(this,
+ "per_process_webview_data_test");
+ ProcessGlobalConfig.apply(config);
+ setContentView(R.layout.activity_data_directory_config);
+ WebView wv = findViewById(R.id.data_directory_config_webview);
+ wv.getSettings().setJavaScriptEnabled(true);
+ wv.setWebViewClient(new WebViewClient());
+ wv.loadUrl("www.google.com");
+ TextView tv = findViewById(R.id.data_directory_config_textview);
+ tv.setText("WebView Loaded!");
+ }
+}
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 b6fb511..0bd1d67 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,43 +17,33 @@
package com.example.androidx.webkit;
import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
import android.os.Bundle;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
-import androidx.webkit.ProcessGlobalConfig;
-import androidx.webkit.WebViewFeature;
-
/**
- * An {@link Activity} which makes use of {@link androidx.webkit.ProcessGlobalConfig} to set up
- * process global configuration prior to loading WebView.
+ * An {@link Activity} which lists features that make use of
+ * {@link androidx.webkit.ProcessGlobalConfig} to set up process global configuration prior to
+ * loading WebView.
*/
public class ProcessGlobalConfigActivity extends AppCompatActivity {
+
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
setTitle(R.string.process_global_config_activity_title);
WebkitHelpers.appendWebViewVersionToTitle(this);
-
- if (!WebViewFeature.isStartupFeatureSupported(this,
- WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) {
- WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
- return;
- }
- ProcessGlobalConfig config = new ProcessGlobalConfig();
- config.setDataDirectorySuffix(this,
- "per_process_webview_data_test");
- ProcessGlobalConfig.apply(config);
- setContentView(R.layout.activity_process_global_config);
- WebView wv = findViewById(R.id.process_global_config_webview);
- wv.getSettings().setJavaScriptEnabled(true);
- wv.setWebViewClient(new WebViewClient());
- wv.loadUrl("www.google.com");
- TextView tv = findViewById(R.id.process_global_textview);
- tv.setText("WebView Loaded!");
+ final Context activityContext = this;
+ MenuListView listView = findViewById(R.id.top_level_list);
+ MenuListView.MenuItem[] menuItems = new MenuListView.MenuItem[] {
+ new MenuListView.MenuItem(
+ getResources().getString(R.string.data_directory_suffix_activity_title),
+ new Intent(activityContext, DataDirectorySuffixActivity.class)),
+ };
+ listView.setItems(menuItems);
}
}
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_data_directory_config.xml
similarity index 91%
rename from webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
rename to webkit/integration-tests/testapp/src/main/res/layout/activity_data_directory_config.xml
index e898b1d..3e41dab 100644
--- a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_data_directory_config.xml
@@ -24,13 +24,13 @@
tools:ignore="Orientation">
<TextView
- android:id="@+id/process_global_textview"
+ android:id="@+id/data_directory_config_textview"
android:layout_width="wrap_content"
android:layout_height="105dp"
android:text="TextView" />
<WebView
- android:id="@+id/process_global_config_webview"
+ android:id="@+id/data_directory_config_webview"
android:layout_width="match_parent"
android:layout_height="604dp"></WebView>
</LinearLayout>
diff --git a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index fd3734d..7108765 100644
--- a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -100,6 +100,7 @@
<!-- Process Global Config -->
<string name="process_global_config_activity_title">Process Global Config</string>
+ <string name="data_directory_suffix_activity_title">Data Directory Suffix</string>
<!-- Requested With Header -->
<string name="requested_with_activity_title">X-Requested-With header allow-list</string>