Merge changes from topic "index_order" into androidx-main
* changes:
Use enums for @Index#Order API
Workaround KSP enum annotation value inconsistency.
Adding support for column index order in @Index
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index c9c5b9e..5bd8c97 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -26,13 +26,13 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
- api(projectOrArtifact(":compose:runtime:runtime"))
- api(projectOrArtifact(":compose:runtime:runtime-saveable"))
+ api("androidx.compose.runtime:runtime:1.0.1")
+ api("androidx.compose.runtime:runtime-saveable:1.0.1")
api(projectOrArtifact(":activity:activity-ktx"))
- api("androidx.compose.ui:ui:1.0.0-rc02")
+ api("androidx.compose.ui:ui:1.0.1")
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index db3752a..4a7171c 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -25,14 +25,14 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
compileOnly projectOrArtifact(":annotation:annotation-sampled")
- implementation "androidx.compose.foundation:foundation:1.0.0-rc02"
+ implementation "androidx.compose.foundation:foundation:1.0.1"
implementation projectOrArtifact(":activity:activity-compose")
implementation projectOrArtifact(":activity:activity-ktx")
- implementation "androidx.compose.material:material:1.0.0-rc02"
+ implementation "androidx.compose.material:material:1.0.1"
}
androidx {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/FileLinkingRule.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/FileLinkingRule.kt
new file mode 100644
index 0000000..072e1d0f
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/FileLinkingRule.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
+import androidx.benchmark.macro.perfetto.UiState
+import androidx.benchmark.macro.perfetto.appendUiState
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import java.io.File
+
+/**
+ * Rule to enable linking files and traces to Studio UI for macrobench correctness tests.
+ *
+ * Filepaths are registered, and reported, but files are not created by this class, that should
+ * be handled by the test. Ensure you don't clean up the file - it needs to persist to be copied
+ * over by Studio.
+ */
+class FileLinkingRule : TestRule {
+ private lateinit var currentDescription: Description
+ private var summaryString = ""
+
+ private fun createReportedFilePath(
+ label: String,
+ @Suppress("SameParameterValue") extension: String,
+ ): String {
+ // remove parens / brackets, as it confuses linking
+ val methodLabel = currentDescription.toUniqueName()
+ .replace("(", "_")
+ .replace(")", "_")
+ .replace("[", "_")
+ .replace("]", "_")
+
+ val file = File(
+ Outputs.dirUsableByAppAndShell,
+ "${label}_${Outputs.dateToFileName()}.$extension"
+ )
+ val absolutePath: String = file.absolutePath
+ val relativePath = Outputs.relativePathFor(absolutePath)
+
+ summaryString += "$methodLabel [$label](file://$relativePath)\n"
+ return absolutePath
+ }
+
+ /**
+ * Map of trace abs path -> process to highlight.
+ *
+ * After trace is complete (at end of test), we write a UI state packet to it, so trace UI
+ * can highlight/select the relevant process.
+ */
+ private val traceToPackageMap = mutableMapOf<String, String>()
+
+ fun createReportedTracePath(
+ packageName: String,
+ label: String = "trace"
+ ): String {
+ val absolutePath = createReportedFilePath(label, "perfetto-trace")
+ traceToPackageMap[absolutePath] = packageName
+ return absolutePath
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return RuleChain
+ .outerRule(::applyInternal)
+ .apply(base, description)
+ }
+
+ private fun applyInternal(base: Statement, description: Description) = object : Statement() {
+ override fun evaluate() {
+ require(Outputs.outputDirectory == Outputs.dirUsableByAppAndShell) {
+ "FileLinkingRule may only be used when outputDirectory == dirUsableByAppAndShell"
+ }
+
+ currentDescription = description
+ try {
+ base.evaluate()
+ } finally {
+ flush()
+ }
+ }
+ }
+
+ private fun flush() {
+ traceToPackageMap.forEach { entry ->
+ File(entry.key).apply {
+ if (exists()) {
+ appendUiState(
+ UiState(null, null, entry.value)
+ )
+ }
+ }
+ }
+
+ InstrumentationResults.instrumentationReport {
+ ideSummaryRecord(
+ summaryV1 = "", // not supported
+ summaryV2 = summaryString.trim()
+ )
+ }
+ }
+
+ private fun Description.toUniqueName() = testClass.simpleName + "_" + methodName
+}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index b2843e0..2c15264 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -18,6 +18,7 @@
import android.content.Intent
import android.content.pm.PackageManager
+import androidx.benchmark.macro.perfetto.isShellSessionRooted
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -37,38 +38,38 @@
@SdkSuppress(minSdkVersion = 27) // Lowest version validated
@RunWith(AndroidJUnit4::class)
@LargeTest
-public class MacrobenchmarkScopeTest {
+class MacrobenchmarkScopeTest {
@Before
fun setup() {
// validate target is installed with clear error message,
// since error messages from e.g. startActivityAndWait may be less clear
try {
val pm = InstrumentationRegistry.getInstrumentation().context.packageManager
- pm.getApplicationInfo(TARGET_PACKAGE_NAME, 0)
+ pm.getApplicationInfo(Packages.TARGET, 0)
} catch (notFoundException: PackageManager.NameNotFoundException) {
throw IllegalStateException(
- "Unable to find target $TARGET_PACKAGE_NAME, is it installed?"
+ "Unable to find target ${Packages.TARGET}, is it installed?"
)
}
}
@Test
- public fun killTest() {
- val scope = MacrobenchmarkScope(TARGET_PACKAGE_NAME, launchWithClearTask = true)
+ fun killTest() {
+ val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
scope.pressHome()
scope.startActivityAndWait()
- assertTrue(isProcessAlive(TARGET_PACKAGE_NAME))
+ assertTrue(isProcessAlive(Packages.TARGET))
scope.killProcess()
- assertFalse(isProcessAlive(TARGET_PACKAGE_NAME))
+ assertFalse(isProcessAlive(Packages.TARGET))
}
@Test
- public fun compile_speedProfile() {
- val scope = MacrobenchmarkScope(TARGET_PACKAGE_NAME, launchWithClearTask = true)
+ fun compile_speedProfile() {
+ val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
val iterations = 1
var executions = 0
val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
- compilation.compile(TARGET_PACKAGE_NAME) {
+ compilation.compile(Packages.TARGET) {
executions += 1
scope.pressHome()
scope.startActivityAndWait()
@@ -77,25 +78,24 @@
}
@Test
- public fun compile_speed() {
+ fun compile_speed() {
val compilation = CompilationMode.Speed
- compilation.compile(TARGET_PACKAGE_NAME) {
+ compilation.compile(Packages.TARGET) {
fail("Should never be called for $compilation")
}
}
@Test
- public fun startActivityAndWait_activityNotExported() {
- val scope = MacrobenchmarkScope(TARGET_PACKAGE_NAME, launchWithClearTask = true)
+ fun startActivityAndWait_activityNotExported() {
+ val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
scope.pressHome()
val intent = Intent()
- intent.setPackage(TARGET_PACKAGE_NAME)
- intent.action = "$TARGET_PACKAGE_NAME.NOT_EXPORTED_ACTIVITY"
+ intent.setPackage(Packages.TARGET)
+ intent.action = "${Packages.TARGET}.NOT_EXPORTED_ACTIVITY"
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
- val prop = device.executeShellCommand("getprop service.adb.root").trim()
- if (prop == "1") {
+ if (device.isShellSessionRooted()) {
// while device and adb session are both rooted, doesn't throw
scope.startActivityAndWait(intent)
} else {
@@ -112,13 +112,13 @@
}
@Test
- public fun startActivityAndWait_invalidActivity() {
- val scope = MacrobenchmarkScope(TARGET_PACKAGE_NAME, launchWithClearTask = true)
+ fun startActivityAndWait_invalidActivity() {
+ val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
scope.pressHome()
val intent = Intent()
intent.setPackage("this.is.not.a.real.package")
- intent.action = "$TARGET_PACKAGE_NAME.NOT_EXPORTED_ACTIVITY"
+ intent.action = "${Packages.TARGET}.NOT_EXPORTED_ACTIVITY"
// should throw, unable to resolve Intent
val exceptionMessage = assertFailsWith<IllegalStateException> {
@@ -129,8 +129,11 @@
}
@Test
- public fun startActivityAndWait_sameActivity() {
- val scope = MacrobenchmarkScope(LOCAL_PACKAGE_NAME, launchWithClearTask = true)
+ fun startActivityAndWait_sameActivity() {
+ val scope = MacrobenchmarkScope(
+ Packages.TEST, // self-instrumenting macrobench, so don't kill the process!
+ launchWithClearTask = true
+ )
val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// Launch first activity, and validate it is displayed
@@ -160,13 +163,4 @@
private fun isProcessAlive(packageName: String): Boolean {
return processes().any { it.contains(packageName) }
}
-
- public companion object {
- // Separate target app. Use this app/package if killing/compiling target process.
- private const val TARGET_PACKAGE_NAME =
- "androidx.benchmark.integration.macrobenchmark.target"
-
- // This test app. Use this app/package if not killing/compiling target.
- private const val LOCAL_PACKAGE_NAME = "androidx.benchmark.macro.test"
- }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MetricResultExtensionsTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MetricResultExtensionsTest.kt
index 8f8e211..bd28aa2 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MetricResultExtensionsTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MetricResultExtensionsTest.kt
@@ -19,18 +19,15 @@
import androidx.benchmark.MetricResult
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-import kotlin.test.assertNotNull
@RunWith(AndroidJUnit4::class)
@SmallTest
-public class MetricResultExtensionsTest {
+class MetricResultExtensionsTest {
@Test
- public fun mergeToMetricResults_trivial() {
+ fun mergeToMetricResults_trivial() {
assertEquals(
expected = listOf(
// note, bar sorted first
@@ -44,7 +41,7 @@
}
@Test
- public fun mergeToMetricResults_standard() {
+ fun mergeToMetricResults_standard() {
assertEquals(
expected = listOf(
// note, bar sorted first
@@ -60,19 +57,19 @@
}
@Test
- public fun mergeToMetricResults_missingKey() {
- val exception = assertFailsWith<IllegalStateException> {
- listOf(
+ fun mergeToMetricResults_missingKey() {
+ assertEquals(
+ expected = listOf(
+ MetricResult("bar", longArrayOf(101, 201)),
+ MetricResult("foo", longArrayOf(100, 200))
+ ),
+ actual = listOf(
mapOf("foo" to 100L, "bar" to 101L),
- mapOf("foo" to 300L), // bar missing! Time to crash!
+ mapOf("foo" to 300L), // bar missing! Skip this iteration!
mapOf("foo" to 200L, "bar" to 201L),
).mergeToMetricResults(
tracePaths = listOf("trace1.trace", "trace2.trace", "trace3.trace")
)
- }
- val message = exception.message
- assertNotNull(message)
- assertTrue(message.contains("Iteration 1 missing keys [bar]"))
- assertTrue(message.contains("trace1.trace\ntrace2.trace\ntrace3.trace"))
+ )
}
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
new file mode 100644
index 0000000..7cc468e
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+object Packages {
+ /**
+ * Separate target app.
+ *
+ * Use this app/package if it's necessary to kill/compile target process.
+ */
+ const val TARGET =
+ "androidx.benchmark.integration.macrobenchmark.target"
+
+ /**
+ * This test app - this process.
+ *
+ * Preferably use this app/package if not killing/compiling target.
+ */
+ const val TEST = "androidx.benchmark.macro.test"
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AtraceTagTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AtraceTagTest.kt
new file mode 100644
index 0000000..4dec363
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/AtraceTagTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro.perfetto
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import org.junit.Assume
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertContains
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+@SmallTest
+class AtraceTagTest {
+ private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ private val shellSessionRooted = device.isShellSessionRooted()
+
+ @Test
+ fun atraceListCategories_readable() {
+ val results = device.executeShellCommand("atrace --list_categories")
+ assertNotEquals("", results)
+ }
+
+ private fun getActualSupportedTags(): Set<String> {
+ val results = device.executeShellCommand("atrace --list_categories")
+
+ assertNotEquals("", results)
+ val actualSupportedTags = results
+ .split("\n")
+ .map {
+ println("captured $it")
+ it.trim().split(" ").first()
+ }
+ .filter { it.isNotBlank() }
+ .toSet()
+
+ // verify able to read stdout with guaranteed tag
+ assertContains(actualSupportedTags, "view")
+
+ return actualSupportedTags
+ }
+
+ @Test
+ fun atraceListCategories_unsupported() {
+ val actualSupportedTags = getActualSupportedTags()
+
+ val expectedUnsupportedTags = AtraceTag.unsupported(rooted = shellSessionRooted)
+ val unexpectedTags = expectedUnsupportedTags.intersect(actualSupportedTags)
+ assertEquals(setOf(), unexpectedTags, "Tags expected to be unsupported weren't")
+ }
+
+ @Test
+ fun atraceListCategories_supported() {
+ val actualSupportedTags = getActualSupportedTags()
+ val expectedSupportedTags = AtraceTag.supported(rooted = shellSessionRooted)
+ .map { it.tag }
+ .toSet()
+
+ val missingTags = expectedSupportedTags - actualSupportedTags
+ assertEquals(setOf(), missingTags, "Tags expected to be supported weren't")
+ }
+
+ @Test
+ fun shellSession_root() {
+ Assume.assumeTrue(shellSessionRooted)
+ }
+
+ @Test
+ fun shellSession_unroot() {
+ Assume.assumeFalse(shellSessionRooted)
+ }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
index b8cbc9f..fccb981 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoCaptureTest.kt
@@ -17,7 +17,8 @@
package androidx.benchmark.macro.perfetto
import android.os.Build
-import androidx.benchmark.Outputs
+import androidx.benchmark.macro.FileLinkingRule
+import androidx.benchmark.macro.Packages
import androidx.benchmark.macro.perfetto.PerfettoHelper.Companion.isAbiSupported
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
@@ -28,29 +29,31 @@
import org.junit.Assert.assertTrue
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
-import java.io.File
+import kotlin.test.assertEquals
@SdkSuppress(minSdkVersion = 29) // Lower to 21 after fixing trace config.
@RunWith(Parameterized::class)
-public class PerfettoCaptureTest(private val unbundled: Boolean) {
- private val traceFile = File(Outputs.dirUsableByAppAndShell, "PerfettoCaptureTest.trace")
- private val traceFilePath = traceFile.absolutePath
+class PerfettoCaptureTest(private val unbundled: Boolean) {
+ @get:Rule
+ val linkRule = FileLinkingRule()
@Before
@After
- public fun cleanup() {
+ fun cleanup() {
PerfettoCapture(unbundled).cancel()
- traceFile.delete()
}
@LargeTest
@Test
- public fun traceAndCheckFileSize() {
+ fun captureAndValidateTrace() {
// Change the check to API >=21, once we have the correct Perfetto config.
assumeTrue(Build.VERSION.SDK_INT >= 29 && isAbiSupported())
+
+ val traceFilePath = linkRule.createReportedTracePath(Packages.TEST)
val perfettoCapture = PerfettoCapture(unbundled)
verifyTraceEnable(false)
@@ -59,27 +62,40 @@
verifyTraceEnable(true)
- trace("PerfettoCaptureTest") {
+ // TODO: figure out why this sleep (200ms+) is needed - possibly related to b/194105203
+ Thread.sleep(500)
+
+ trace(TRACE_SECTION_LABEL) {
// Tracing non-trivial duration for manual debugging/verification
Thread.sleep(20)
}
perfettoCapture.stop(traceFilePath)
- val length = traceFile.length()
- assertTrue("Expect > 10KiB file, was $length bytes", length > 10 * 1024)
+ val matchingSlices = PerfettoTraceProcessor.querySlices(
+ absoluteTracePath = traceFilePath,
+ TRACE_SECTION_LABEL
+ )
+
+ assertEquals(1, matchingSlices.size)
+ matchingSlices.first().apply {
+ assertEquals(TRACE_SECTION_LABEL, name)
+ assertTrue(dur > 15_000_000) // should be at least 15ms
+ }
}
- public companion object {
+ companion object {
+ const val TRACE_SECTION_LABEL = "PerfettoCaptureTest"
+
@Parameterized.Parameters(name = "unbundled={0}")
@JvmStatic
- public fun parameters(): Array<Any> {
+ fun parameters(): Array<Any> {
return arrayOf(true, false)
}
}
}
-public fun verifyTraceEnable(enabled: Boolean) {
+fun verifyTraceEnable(enabled: Boolean) {
// We poll here, since we may need to wait for enable flags to propagate to apps
verifyWithPolling(
"Timeout waiting for Trace.isEnabled == $enabled",
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoConfigTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoConfigTest.kt
index f8e1c77..7259dac 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoConfigTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoConfigTest.kt
@@ -21,7 +21,11 @@
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
+import perfetto.protos.DataSourceConfig
+import perfetto.protos.FtraceConfig
+import perfetto.protos.TraceConfig
import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
@@ -29,7 +33,7 @@
@SmallTest
class PerfettoConfigTest {
@Test
- public fun ftraceBasics() {
+ fun ftraceBasics() {
val ftraceDataSource =
PERFETTO_CONFIG.data_sources.first { it.config?.name == "linux.ftrace" }
@@ -44,4 +48,37 @@
assertFalse(ftraceConfig.atrace_categories.contains("webview"))
assertFalse(ftraceConfig.atrace_categories.contains("memory"))
}
+
+ @Test
+ fun validateAndEncode() {
+ // default config shouldn't throw
+ PERFETTO_CONFIG.validateAndEncode()
+ }
+
+ @Test
+ fun validateAndEncode_invalidAtraceCategories() {
+ val invalidConfig = TraceConfig(
+ buffers = listOf(
+ TraceConfig.BufferConfig(
+ size_kb = 16384,
+ fill_policy = TraceConfig.BufferConfig.FillPolicy.RING_BUFFER
+ )
+ ),
+ data_sources = listOf(
+ TraceConfig.DataSource(
+ config = DataSourceConfig(
+ name = "linux.ftrace",
+ target_buffer = 0,
+ ftrace_config = FtraceConfig(
+ atrace_categories = listOf("bad_category")
+ ),
+ )
+ )
+ )
+ )
+ val exception = assertFailsWith<IllegalStateException> {
+ invalidConfig.validateAndEncode()
+ }
+ assertTrue(exception.message!!.contains("bad_category"))
+ }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/ShellUtilsTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/ShellUtilsTest.kt
index f2a3485..90bc15a 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/ShellUtilsTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/ShellUtilsTest.kt
@@ -28,19 +28,20 @@
@MediumTest
@RunWith(AndroidJUnit4::class)
-public class ShellUtilsTest {
+class ShellUtilsTest {
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@SdkSuppress(minSdkVersion = 23) // broken below 23
@Test
- public fun trivial() {
+ fun trivial() {
assertEquals("foo\n", device.executeShellScript("echo foo"))
assertEquals(ShellOutput("foo\n", ""), device.executeShellScriptWithStderr("echo foo"))
}
+ @SdkSuppress(minSdkVersion = 23) // broken below 23
@Test
- public fun trivialStderr() {
+ fun trivialStderr() {
val shellOutput = device.executeShellScriptWithStderr("invalidCommand")
assertEquals("", shellOutput.stdout)
@@ -58,7 +59,7 @@
@SdkSuppress(minSdkVersion = 26) // xargs only available before 26
@Test
- public fun pipe_xargs() {
+ fun pipe_xargs() {
// validate piping works with xargs
assertEquals("foo\n", device.executeShellScript("echo foo | xargs echo $1"))
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
index 49fb9f4..10094cb 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MetricResultExtensions.kt
@@ -16,38 +16,29 @@
package androidx.benchmark.macro
+import android.util.Log
import androidx.benchmark.MetricResult
/**
* Merge the Map<String, Long> results from each iteration into one List<MetricResult>
*/
internal fun List<Map<String, Long>>.mergeToMetricResults(
- tracePaths: List<String>
+ @Suppress("UNUSED_PARAMETER") tracePaths: List<String>
): List<MetricResult> {
val setOfAllKeys = flatMap { it.keys }.toSet()
- // validate each key shows up in each iteration
- val iterationErrorStrings = mapIndexedNotNull { iteration, iterationResults ->
- if (iterationResults.keys != setOfAllKeys) {
- "Iteration $iteration missing keys " + (setOfAllKeys - iterationResults.keys)
- } else null
- }
- if (iterationErrorStrings.isNotEmpty()) {
- throw IllegalStateException(
- "Error, different metrics observed in different iterations.\n\n" +
- iterationErrorStrings.joinToString("\n") +
- "Please report a bug, and include a logcat capture, and all traces captured by " +
- "this test run:\n" + tracePaths.joinToString("\n") + "\n" +
- DeviceInfo.deviceSummaryString
- )
- }
-
// build Map<String, List<Long>>
- val listResults: Map<String, List<Long>> = setOfAllKeys.map { key ->
- key to map {
- it[key] ?: error("Metric $key not observed in iteration")
+ val listResults: Map<String, List<Long>> = setOfAllKeys.associateWith { key ->
+ mapIndexedNotNull { iteration, resultMap ->
+ if (resultMap.keys != setOfAllKeys) {
+ // TODO: assert that metrics are always captured (b/193827052)
+ Log.d(TAG, "Skipping results from iter $iteration, it didn't capture all metrics")
+ null
+ } else {
+ resultMap[key] ?: error("Metric $key not observed in iteration")
+ }
}
- }.toMap()
+ }
// transform to List<MetricResult>, sorted by metric name
return listResults.map { (metricName, values) ->
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AtraceTag.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AtraceTag.kt
new file mode 100644
index 0000000..ef21283
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/AtraceTag.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro.perfetto
+
+import android.os.Build
+
+/**
+ * Enum representing set of all atrace tags used by macrobenchmark, and whether they're expected to
+ * be supported on the current device.
+ *
+ * Note that this API assumes API >= 21, as that's the library's min API
+ *
+ * While supported tags could be collected from the local device (e.g. in `AtraceTagTest`), the
+ * intent of this class is to track this information statically.
+ */
+@Suppress("unused") // enums always accessed via values()
+internal enum class AtraceTag(
+ val tag: String
+) {
+ ActivityManager("am"),
+ BinderDriver("binder_driver") {
+ override fun supported(api: Int, rooted: Boolean): Boolean {
+ return api >= 24
+ }
+ },
+ Camera("camera"),
+ Dalvik("dalvik"),
+ Frequency("freq"),
+ Graphics("gfx"),
+ HardwareModules("hal"),
+ Idle("idle"),
+ Input("input"),
+ MemReclaim("memreclaim"),
+ Resources("res"),
+ Scheduling("sched"),
+ Synchronization("sync") {
+ override fun supported(api: Int, rooted: Boolean): Boolean {
+ return rooted || api >= 28
+ }
+ },
+ View("view"),
+ WebView("webview"),
+ WindowManager("wm");
+
+ /**
+ * Return true if the tag is available on the specified api level, with specified shell
+ * session root status.
+ */
+ open fun supported(api: Int, rooted: Boolean): Boolean {
+ return true
+ }
+
+ companion object {
+ fun supported(
+ api: Int = Build.VERSION.SDK_INT,
+ rooted: Boolean
+ ): Set<AtraceTag> {
+ return values()
+ .filter { it.supported(api = api, rooted = rooted) }
+ .toSet()
+ }
+
+ fun unsupported(
+ api: Int = Build.VERSION.SDK_INT,
+ rooted: Boolean
+ ): Set<AtraceTag> {
+ return values().toSet() - supported(api, rooted)
+ }
+ }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
index f3c5611..d7a9430 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoCapture.kt
@@ -60,7 +60,7 @@
val configProtoFile = File(Outputs.dirUsableByAppAndShell, "trace_config.pb")
try {
userspaceTrace("write config") {
- configProtoFile.writeBytes(PERFETTO_CONFIG.encode())
+ configProtoFile.writeBytes(PERFETTO_CONFIG.validateAndEncode())
}
userspaceTrace("start perfetto process") {
helper.startCollecting(configProtoFile.absolutePath, false)
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoConfig.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoConfig.kt
index 464fd24..1a6d08d 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoConfig.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoConfig.kt
@@ -16,6 +16,9 @@
package androidx.benchmark.macro.perfetto
+import android.os.Build
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
import perfetto.protos.DataSourceConfig
import perfetto.protos.FtraceConfig
import perfetto.protos.MeminfoCounters
@@ -54,26 +57,31 @@
"lowmemorykiller/lowmemory_kill",
),
atrace_categories = listOf(
- // atrace categories currently come from default systrace tags
- "am",
- "binder_driver",
- "camera",
- "dalvik",
- "freq",
- "gfx",
- "hal",
- "idle",
- "input",
- "memreclaim",
- "res",
- "sched",
- "sync",
- "view",
+ AtraceTag.ActivityManager,
+ AtraceTag.BinderDriver,
+ AtraceTag.Camera,
+ AtraceTag.Dalvik,
+ AtraceTag.Frequency,
+ AtraceTag.Graphics,
+ AtraceTag.HardwareModules,
+ AtraceTag.Idle,
+ AtraceTag.Input,
+ AtraceTag.MemReclaim,
+ AtraceTag.Resources,
+ AtraceTag.Scheduling,
+ AtraceTag.Synchronization,
+ AtraceTag.View,
+ AtraceTag.WindowManager
// "webview" not included to workaround b/190743595
- "wm",
// "memory" not included as some Q devices requiring ftrace_event
// configuration directly to collect this data. See b/171085599
- ),
+ ).filter {
+ // filter to only supported tags on unrooted build
+ // TODO: use root-only tags as needed
+ it.supported(api = Build.VERSION.SDK_INT, rooted = false)
+ }.map {
+ it.tag
+ },
// Trace all apps for now
atrace_apps = listOf("*")
)
@@ -137,4 +145,23 @@
PROCESS_STATS_DATASOURCE,
LINUX_SYS_STATS_DATASOURCE,
),
-)
\ No newline at end of file
+)
+
+internal fun TraceConfig.validateAndEncode(): ByteArray {
+ val ftraceConfig = data_sources
+ .mapNotNull { it.config?.ftrace_config }
+ .first()
+
+ // check tags against known-supported tags based on SDK_INT / root status
+ val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+ val supportedTags = AtraceTag.supported(
+ api = Build.VERSION.SDK_INT,
+ rooted = device.isShellSessionRooted()
+ ).map { it.tag }.toSet()
+
+ val unsupportedTags = (ftraceConfig.atrace_categories - supportedTags)
+ check(unsupportedTags.isEmpty()) {
+ "Error - attempted to use unsupported atrace tags: $unsupportedTags"
+ }
+ return encode()
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
index 8fdab8f..2646dad 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
@@ -18,6 +18,7 @@
import android.util.Log
import androidx.annotation.RequiresApi
+import androidx.benchmark.Outputs
import androidx.benchmark.macro.device
import androidx.benchmark.macro.userspaceTrace
import androidx.test.platform.app.InstrumentationRegistry
@@ -42,10 +43,14 @@
PerfettoHelper.createExecutable("trace_processor_shell")
}
- fun getJsonMetrics(absoluteTracePath: String, metric: String): String {
+ private fun validateTracePath(absoluteTracePath: String) {
require(!absoluteTracePath.contains(" ")) {
"Trace path must not contain spaces: $absoluteTracePath"
}
+ }
+
+ fun getJsonMetrics(absoluteTracePath: String, metric: String): String {
+ validateTracePath(absoluteTracePath)
require(!metric.contains(" ")) {
"Metric must not contain spaces: $metric"
}
@@ -69,4 +74,78 @@
}
return json
}
+
+ data class Slice(
+ val name: String,
+ val ts: Long,
+ val dur: Long
+ )
+
+ private fun String.unquote(): String {
+ require(this.first() == '"' && this.last() == '"')
+ return this.substring(1, length - 1)
+ }
+
+ /**
+ * Query a trace for a list of slices - name, timestamp, and duration.
+ */
+ fun querySlices(
+ absoluteTracePath: String,
+ vararg sliceNames: String
+ ): List<Slice> {
+ val whereClause = sliceNames
+ .joinToString(separator = " AND ") {
+ "slice.name = '$it'"
+ }
+
+ val queryResult = rawQuery(
+ absoluteTracePath = absoluteTracePath,
+ query = """
+ SELECT slice.name,ts,dur
+ FROM slice
+ JOIN thread_track ON thread_track.id = slice.track_id
+ WHERE $whereClause
+ """.trimMargin()
+ )
+ val resultLines = queryResult.split("\n")
+
+ if (resultLines.first() != "\"name\",\"ts\",\"dur\"") {
+ throw IllegalStateException("query failed!")
+ }
+
+ // results are in CSV with a header row, and strings wrapped with quotes
+ return resultLines
+ .filter { it.isNotBlank() } // drop blank lines
+ .drop(1) // drop the header row
+ .map {
+ val columns = it.split(",")
+ Slice(
+ name = columns[0].unquote(),
+ ts = columns[1].toLong(),
+ dur = columns[2].toLong()
+ )
+ }
+ }
+
+ private fun rawQuery(
+ absoluteTracePath: String,
+ query: String
+ ): String {
+ validateTracePath(absoluteTracePath)
+
+ val queryFile = File(Outputs.dirUsableByAppAndShell, "trace_processor_query.sql")
+ try {
+ queryFile.writeText(query)
+
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ val device = instrumentation.device()
+
+ val command = "$shellPath --query-file ${queryFile.absolutePath} $absoluteTracePath"
+ return userspaceTrace("trace_processor_shell") {
+ device.executeShellCommand(command)
+ }
+ } finally {
+ queryFile.delete()
+ }
+ }
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
index 89bddae..157ed3a 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/ShellUtils.kt
@@ -163,6 +163,14 @@
return runnableExecutablePath
}
+/**
+ * Returns true if the shell session is rooted, and thus root commands can be run (e.g. atrace
+ * commands with root-only tags)
+ */
+internal fun UiDevice.isShellSessionRooted(): Boolean {
+ return executeShellCommand("getprop service.adb.root").trim() == "1"
+}
+
private fun UiDevice.moveToTmpAndMakeExecutable(src: String, dst: String) {
// Note: we don't check for return values from the below, since shell based file
// permission errors generally crash our process.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
index 3cd2d2b..1619e7f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -80,13 +80,4 @@
executor.execute { callback.onCaptureCancelled() }
}
}
-
- // TODO(b/186853278): Look into why onComplete isn't called
- override fun onTotalCaptureResult(
- requestMetadata: RequestMetadata,
- frameNumber: FrameNumber,
- totalCaptureResult: FrameInfo
- ) {
- onComplete(requestMetadata, frameNumber, totalCaptureResult)
- }
}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
index cf5b9d0..29254f2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ExcludedSupportedSizesQuirk.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.camera.core.Logger;
+import androidx.camera.core.impl.ImageFormatConstants;
import androidx.camera.core.impl.Quirk;
import java.util.ArrayList;
@@ -34,13 +35,17 @@
* <p>
* An example is the resolution size 4000x3000 which is supported on OnePlus 6, but causes a WYSIWYG
* issue between preview and image capture. See b/157448499.
+ *
+ * <p>On Huawei P20 Lite, the Preview screen will become too bright when 400x400 or 720x720
+ * Preview resolutions are used together with a large zoom in value. The same symptom happens on
+ * ImageAnalysis. See https://issuetracker.google.com/192129158.
*/
public class ExcludedSupportedSizesQuirk implements Quirk {
private static final String TAG = "ExcludedSupportedSizesQuirk";
static boolean load() {
- return isOnePlus6() || isOnePlus6T();
+ return isOnePlus6() || isOnePlus6T() || isHuaweiP20Lite();
}
private static boolean isOnePlus6() {
@@ -52,6 +57,10 @@
Build.DEVICE);
}
+ private static boolean isHuaweiP20Lite() {
+ return "HUAWEI".equalsIgnoreCase(Build.BRAND) && "HWANE".equalsIgnoreCase(Build.DEVICE);
+ }
+
/**
* Retrieves problematic supported surface sizes that have to be excluded on the current
* device, for the given camera id and image format.
@@ -64,6 +73,9 @@
if (isOnePlus6T()) {
return getOnePlus6TExcludedSizes(cameraId, imageFormat);
}
+ if (isHuaweiP20Lite()) {
+ return getHuaweiP20LiteExcludedSizes(cameraId, imageFormat);
+ }
Logger.w(TAG, "Cannot retrieve list of supported sizes to exclude on this device.");
return Collections.emptyList();
}
@@ -87,4 +99,16 @@
}
return sizes;
}
+
+ @NonNull
+ private List<Size> getHuaweiP20LiteExcludedSizes(@NonNull String cameraId, int imageFormat) {
+ final List<Size> sizes = new ArrayList<>();
+ if (cameraId.equals("0")
+ && (imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE
+ || imageFormat == ImageFormat.YUV_420_888)) {
+ sizes.add(new Size(720, 720));
+ sizes.add(new Size(400, 400));
+ }
+ return sizes;
+ }
}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
index 748720f..be4979d 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/ExcludedSupportedSizesContainerTest.java
@@ -18,6 +18,7 @@
import static android.graphics.ImageFormat.JPEG;
import static android.graphics.ImageFormat.PRIVATE;
+import static android.graphics.ImageFormat.YUV_420_888;
import static com.google.common.truth.Truth.assertThat;
@@ -48,6 +49,8 @@
private static final Size SIZE_4000_3000 = new Size(4000, 3000);
private static final Size SIZE_4160_3120 = new Size(4160, 3120);
+ private static final Size SIZE_720_720 = new Size(720, 720);
+ private static final Size SIZE_400_400 = new Size(400, 400);
@ParameterizedRobolectricTestRunner.Parameters
public static Collection<Object[]> data() {
@@ -62,6 +65,14 @@
data.add(new Object[]{new Config("OnePlus", "OnePlus6T", "0", PRIVATE)});
data.add(new Object[]{new Config("OnePlus", "OnePlus3", "0", JPEG)});
data.add(new Object[]{new Config(null, null, "0", JPEG)});
+ // Huawei P20 Lite
+ data.add(new Object[]{
+ new Config("HUAWEI", "HWANE", "0", PRIVATE, SIZE_720_720, SIZE_400_400)});
+ data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", PRIVATE)});
+ data.add(new Object[]{
+ new Config("HUAWEI", "HWANE", "0", YUV_420_888, SIZE_720_720, SIZE_400_400)});
+ data.add(new Object[]{new Config("HUAWEI", "HWANE", "1", YUV_420_888)});
+ data.add(new Object[]{new Config("HUAWEI", "HWANE", "0", JPEG)});
return data;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
index f4dae65..5f8c672 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
@@ -34,9 +34,9 @@
private final Object mLock = new Object();
@GuardedBy("mLock")
- private volatile int mOutstandingImages = 0;
+ private int mOutstandingImages = 0;
@GuardedBy("mLock")
- private volatile boolean mIsClosed = false;
+ private boolean mIsClosed = false;
// The wrapped instance of ImageReaderProxy
@GuardedBy("mLock")
@@ -46,7 +46,7 @@
private final Surface mSurface;
// Called after images are closed to check if the ImageReaderProxy should be closed
- private ForwardingImageProxy.OnImageCloseListener mImageCloseListener = (image) -> {
+ private final ForwardingImageProxy.OnImageCloseListener mImageCloseListener = (image) -> {
synchronized (mLock) {
mOutstandingImages--;
if (mIsClosed && mOutstandingImages == 0) {
@@ -97,16 +97,14 @@
@GuardedBy("mLock")
@Nullable
private ImageProxy wrapImageProxy(@Nullable ImageProxy imageProxy) {
- synchronized (mLock) {
- if (imageProxy != null) {
- mOutstandingImages++;
- SingleCloseImageProxy singleCloseImageProxy =
- new SingleCloseImageProxy(imageProxy);
- singleCloseImageProxy.addOnImageCloseListener(mImageCloseListener);
- return singleCloseImageProxy;
- } else {
- return null;
- }
+ if (imageProxy != null) {
+ mOutstandingImages++;
+ SingleCloseImageProxy singleCloseImageProxy =
+ new SingleCloseImageProxy(imageProxy);
+ singleCloseImageProxy.addOnImageCloseListener(mImageCloseListener);
+ return singleCloseImageProxy;
+ } else {
+ return null;
}
}
@@ -117,7 +115,6 @@
* <p>Once this has been called, no more additional ImageProxy can be acquired from the
* {@link SafeCloseImageReaderProxy}.
*/
- @GuardedBy("mLock")
void safeClose() {
synchronized (mLock) {
mIsClosed = true;
diff --git a/camera/camera-video/build.gradle b/camera/camera-video/build.gradle
index 60272a1..9f7c8cc 100644
--- a/camera/camera-video/build.gradle
+++ b/camera/camera-video/build.gradle
@@ -56,6 +56,7 @@
androidTestImplementation(libs.truth)
androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
+ androidTestImplementation(project(":camera:camera-lifecycle"))
androidTestImplementation(project(":camera:camera-testing"))
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.kotlinCoroutinesAndroid)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index c482c6d..d673a33 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -24,16 +24,15 @@
import android.util.Log
import android.util.Size
import android.view.Surface
-import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraInfo
import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraX
import androidx.camera.core.Preview
import androidx.camera.core.impl.utils.CameraOrientationUtil
import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.SurfaceTextureProvider
+import androidx.camera.testing.fakes.FakeLifecycleOwner
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
@@ -61,7 +60,7 @@
val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
companion object {
- private const val VIDEO_TIMEOUT = 10_000L
+ private const val VIDEO_TIMEOUT_SEC = 10L
private const val TAG = "VideoRecordingTest"
@JvmStatic
@Parameterized.Parameters
@@ -75,7 +74,8 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context: Context = ApplicationProvider.getApplicationContext()
- private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
+ private lateinit var cameraProvider: ProcessCameraProvider
+ private lateinit var lifecycleOwner: FakeLifecycleOwner
private lateinit var preview: Preview
private lateinit var cameraInfo: CameraInfo
@@ -112,36 +112,34 @@
@Before
fun setUp() {
Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
- // Skip for b/168175357
+ // Skip test for b/168175357
Assume.assumeFalse(
"Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
)
- CameraX.initialize(context, Camera2Config.defaultConfig()).get()
- cameraUseCaseAdapter = CameraUtil.createCameraUseCaseAdapter(context, cameraSelector)
- cameraInfo = cameraUseCaseAdapter.cameraInfo
+ cameraProvider = ProcessCameraProvider.getInstance(context).get()
+ lifecycleOwner = FakeLifecycleOwner()
+ lifecycleOwner.startAndResume()
// Add extra Preview to provide an additional surface for b/168187087.
preview = Preview.Builder().build()
// Sets surface provider to preview
instrumentation.runOnMainSync {
- preview.setSurfaceProvider(
- getSurfaceProvider()
- )
+ preview.setSurfaceProvider(getSurfaceProvider())
+ cameraInfo = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)
+ .cameraInfo
}
}
@After
fun tearDown() {
- if (this::cameraUseCaseAdapter.isInitialized) {
+ if (this::cameraProvider.isInitialized) {
instrumentation.runOnMainSync {
- cameraUseCaseAdapter.apply {
- removeUseCases(useCases)
- }
+ cameraProvider.unbindAll()
}
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
- CameraX.shutdown().get(10, TimeUnit.SECONDS)
}
@Test
@@ -157,7 +155,7 @@
latchForVideoRecording = CountDownLatch(5)
instrumentation.runOnMainSync {
- cameraUseCaseAdapter.addUseCases(listOf(preview, videoCapture))
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, videoCapture)
}
// Act.
@@ -172,15 +170,13 @@
@Test
fun getCorrectResolution_when_setSupportedQuality() {
+ // Pre-arrange.
Assume.assumeTrue(QualitySelector.getSupportedQualities(cameraInfo).isNotEmpty())
-
val qualityList = QualitySelector.getSupportedQualities(cameraInfo)
- instrumentation.runOnMainSync {
- cameraUseCaseAdapter.addUseCases(listOf(preview))
- }
-
Log.d(TAG, "CameraSelector: ${cameraSelector.lensFacing}, QualityList: $qualityList ")
+
qualityList.forEach loop@{ quality ->
+ // Arrange.
val targetResolution = QualitySelector.getResolution(cameraInfo, quality)
if (targetResolution == null) {
// If targetResolution is null, try next one
@@ -199,7 +195,13 @@
latchForVideoRecording = CountDownLatch(5)
instrumentation.runOnMainSync {
- cameraUseCaseAdapter.addUseCases(listOf(videoCapture))
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ videoCapture
+ )
}
// Act.
@@ -210,31 +212,65 @@
// Cleanup.
file.delete()
- instrumentation.runOnMainSync {
- cameraUseCaseAdapter.apply {
- removeUseCases(listOf(videoCapture))
- }
- }
}
}
+ @Test
+ fun stopRecording_when_useCaseUnbind() {
+ // Arrange.
+ val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
+ val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+ latchForVideoSaved = CountDownLatch(1)
+ latchForVideoRecording = CountDownLatch(5)
+
+ instrumentation.runOnMainSync {
+ cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, videoCapture)
+ }
+
+ // Act.
+ startVideoRecording(videoCapture, file)
+ instrumentation.runOnMainSync {
+ cameraProvider.unbind(videoCapture)
+ }
+
+ // Verify.
+ // Wait for finalize event to saved file.
+ assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+ // Check if any error after recording finalized
+ assertWithMessage(TAG + "Finalize with error: ${finalize.error}, ${finalize.cause}.")
+ .that(finalize.hasError()).isFalse()
+
+ // Cleanup.
+ file.delete()
+ }
+
+ // TODO(b/193385037): Add test to check video-recording stop when lifecylce state is paused.
+
+ private fun startVideoRecording(videoCapture: VideoCapture<Recorder>, file: File):
+ ActiveRecording {
+ val outputOptions = FileOutputOptions.builder().setFile(file).build()
+
+ val activeRecording = videoCapture.output
+ .prepareRecording(outputOptions)
+ .withEventListener(
+ CameraXExecutors.directExecutor(),
+ videoRecordEventListener
+ )
+ .start()
+
+ // Wait for status event to proceed recording for a while.
+ assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+ return activeRecording
+ }
+
private fun completeVideoRecording(videoCapture: VideoCapture<Recorder>, file: File) {
- val outputOptions = FileOutputOptions.builder().setFile(file).build()
-
- val activeRecording = videoCapture.output
- .prepareRecording(outputOptions)
- .withEventListener(
- CameraXExecutors.directExecutor(),
- videoRecordEventListener
- )
- .start()
-
- // Wait for status event to proceed recording for a while.
- assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ val activeRecording = startVideoRecording(videoCapture, file)
activeRecording.stop()
// Wait for finalize event to saved file.
- assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
// Check if any error after recording finalized
assertWithMessage(TAG + "Finalize with error: ${finalize.error}, ${finalize.cause}.")
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
index cb37955..9ac9a24 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/PendingRecording.java
@@ -101,14 +101,19 @@
* {@link RecordingStats#AUDIO_DISABLED} for all {@link RecordingStats} send to the listener
* set by {@link #withEventListener(Executor, Consumer)}.
*
- * <p>This method requires the caller to hold the permission
- * {@link android.Manifest.permission#RECORD_AUDIO}.
+ * <p>Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+ * permission; without it, recording will fail at {@link #start()} with an
+ * {@link IllegalStateException}.
*
* @return this pending recording
+ * @throws IllegalStateException if the {@link Recorder} this recording is associated to
+ * doesn't support audio.
*/
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@NonNull
public PendingRecording withAudioEnabled() {
+ Preconditions.checkState(mRecorder.isAudioSupported(), "The Recorder this recording is "
+ + "associated to doesn't support audio.");
mAudioEnabled = true;
return this;
}
@@ -134,7 +139,8 @@
* {@link ActiveRecording} will be in a finalized state, and all controls will be no-ops.
*
* @throws IllegalStateException if the associated Recorder currently has an unfinished
- * active recording.
+ * active recording, or if the recording has {@link #withAudioEnabled()} audio} but
+ * {@link android.Manifest.permission#RECORD_AUDIO} is not granted.
*/
@NonNull
public ActiveRecording start() {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index e5e52bc9..938cb2b 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -162,10 +162,6 @@
*/
INITIALIZING,
/**
- * Audio recording is not supported by this Recorder.
- */
- UNSUPPORTED,
- /**
* Audio recording is disabled for the running recording.
*/
DISABLED,
@@ -241,9 +237,10 @@
// May be equivalent to mUserProvidedExecutor or an internal executor if the user did not
// provide an executor.
private final Executor mExecutor;
+ private final AtomicBoolean mSurfaceRequested = new AtomicBoolean(false);
+ private final AtomicBoolean mAudioInitialized = new AtomicBoolean(false);
private SurfaceRequest.TransformationInfo mSurfaceTransformationInfo = null;
private Throwable mErrorCause;
- private AtomicBoolean mSurfaceRequested = new AtomicBoolean(false);
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final SparseArray<CallbackToFutureAdapter.Completer<Void>> mEncodingCompleters =
@@ -294,13 +291,8 @@
mSequentialExecutor = CameraXExecutors.newSequentialExecutor(mExecutor);
mMediaSpec = MutableStateObservable.withInitialState(composeRecorderMediaSpec(mediaSpec));
- if (getObservableData(mMediaSpec).getAudioSpec().getChannelCount()
- == AudioSpec.CHANNEL_COUNT_NONE) {
- setAudioState(AudioState.UNSUPPORTED);
- }
}
- @SuppressLint("MissingPermission")
@Override
public void onSurfaceRequested(@NonNull SurfaceRequest request) {
synchronized (mLock) {
@@ -477,26 +469,18 @@
* completes. The recording will be considered active, so before it's finalized, an
* {@link IllegalStateException} will be thrown if this method is called for a second time.
*
- * @throws IllegalStateException if there's an active recording or the Recorder has been
- * released.
+ * @throws IllegalStateException if there's an active recording, or the audio is
+ * {@link PendingRecording#withAudioEnabled() enabled} for the recording but
+ * {@link android.Manifest.permission#RECORD_AUDIO} is not granted.
*/
+ @SuppressLint("MissingPermission")
@NonNull
ActiveRecording start(@NonNull PendingRecording pendingRecording) {
Preconditions.checkNotNull(pendingRecording, "The given PendingRecording cannot be null.");
+ ActiveRecording activeRecording = ActiveRecording.from(pendingRecording);
synchronized (mLock) {
- ActiveRecording activeRecording = ActiveRecording.from(pendingRecording);
- mRunningRecording = activeRecording;
- switch (getObservableData(mState)) {
- case RESETTING:
- // Fall-through
- case INITIALIZING:
- // The recording will automatically start once the initialization completes.
- setState(State.PENDING_RECORDING);
- break;
- case IDLING:
- mSequentialExecutor.execute(this::startInternal);
- setState(State.RECORDING);
- break;
+ State state = getObservableData(mState);
+ switch (state) {
case PENDING_PAUSED:
// Fall-through
case PAUSED:
@@ -505,15 +489,35 @@
// Fall-through
case RECORDING:
throw new IllegalStateException("There's an active recording.");
+ case RESETTING:
+ // Fall-through
+ case INITIALIZING:
+ setupAudioIfNeeded(activeRecording);
+ mRunningRecording = activeRecording;
+ // The recording will automatically start once the initialization completes.
+ setState(State.PENDING_RECORDING);
+ break;
+ case IDLING:
+ setupAudioIfNeeded(activeRecording);
+ mRunningRecording = activeRecording;
+ mSequentialExecutor.execute(this::startInternal);
+ setState(State.RECORDING);
+ break;
case ERROR:
- Logger.e(TAG, "Recording was started when the Recorder had encountered error "
- + mErrorCause);
- finalizeRecording(VideoRecordEvent.ERROR_RECORDER_ERROR, mErrorCause);
+ Logger.e(TAG,
+ "Recording was started when the Recorder had encountered error "
+ + mErrorCause);
+ // Immediately finalize the recording if the Recorder encountered error.
+ activeRecording.updateVideoRecordEvent(VideoRecordEvent.finalizeWithError(
+ activeRecording.getOutputOptions(),
+ getCurrentRecordingStats(),
+ OutputResults.of(Uri.EMPTY),
+ VideoRecordEvent.ERROR_RECORDER_ERROR,
+ mErrorCause));
break;
}
-
- return activeRecording;
}
+ return activeRecording;
}
void pause() {
@@ -646,13 +650,7 @@
}
@ExecutedBy("mSequentialExecutor")
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
private void initializeInternal(SurfaceRequest surfaceRequest) {
- if (mAudioState != AudioState.UNSUPPORTED) {
- // Skip setting up audio as the media spec shows there's no audio channel.
- setupAudio();
- }
-
if (mSurface != null) {
// The video encoder has already be created, providing the surface directly.
surfaceRequest.provideSurface(mSurface, mSequentialExecutor, (result) -> {
@@ -769,9 +767,22 @@
.build();
}
- @ExecutedBy("mSequentialExecutor")
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
- private void setupAudio() {
+ private void setupAudioIfNeeded(@NonNull ActiveRecording activeRecording) {
+ if (!activeRecording.isAudioEnabled()) {
+ // Skip if audio is not enabled for the recording.
+ return;
+ }
+
+ if (!isAudioSupported()) {
+ throw new IllegalStateException("The Recorder doesn't support recording with audio");
+ }
+
+ if (mAudioInitialized.getAndSet(true)) {
+ // Skip if audio has already been initialized.
+ return;
+ }
+
MediaSpec mediaSpec = getObservableData(mMediaSpec);
AudioEncoderConfig config = composeAudioEncoderConfig(mediaSpec);
@@ -793,14 +804,7 @@
mediaSpec.getAudioSpec());
} catch (AudioSourceAccessException e) {
Logger.e(TAG, "Unable to create audio source." + e);
- setState(State.ERROR);
- mErrorCause = e;
- return;
- } catch (SecurityException e) {
- Logger.e(TAG, "Missing audio recording permission." + e);
- setState(State.ERROR);
- mErrorCause = e;
- return;
+ throw new IllegalStateException("Unable to create audio source.", e);
}
mAudioEncoder.setEncoderCallback(new EncoderCallback() {
@@ -878,7 +882,6 @@
}, mSequentialExecutor);
}
- @ExecutedBy("mSequentialExecutor")
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@NonNull
private AudioSource setupAudioSource(@NonNull BufferProvider<InputBuffer> bufferProvider,
@@ -896,8 +899,6 @@
@Override
public void onSilenced(boolean silenced) {
switch (mAudioState) {
- case UNSUPPORTED:
- // Fall-through
case DISABLED:
// Fall-through
case ENCODER_ERROR:
@@ -1235,7 +1236,7 @@
private void resetInternal() {
if (mAudioEncoder != null) {
mAudioEncoder.release();
- mAudioSource = null;
+ mAudioEncoder = null;
}
if (mVideoEncoder != null) {
mVideoEncoder.release();
@@ -1247,13 +1248,12 @@
}
mSurfaceRequested.set(false);
+ mAudioInitialized.set(false);
setState(State.INITIALIZING);
}
private int internalAudioStateToEventAudioState(AudioState audioState) {
switch (audioState) {
- case UNSUPPORTED:
- // Fall-through
case DISABLED:
return RecordingStats.AUDIO_DISABLED;
case INITIALIZING:
@@ -1271,7 +1271,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
boolean isAudioEnabled() {
- return mAudioState != AudioState.UNSUPPORTED && mAudioState != AudioState.DISABLED;
+ return mAudioState != AudioState.DISABLED && mAudioState != AudioState.ENCODER_ERROR;
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -1320,13 +1320,6 @@
mRecordingStopError = VideoRecordEvent.ERROR_UNKNOWN;
mFileSizeLimitInBytes = OutputOptions.FILE_SIZE_UNLIMITED;
- // Reset audio setting to the Recorder default.
- if (getObservableData(mMediaSpec).getAudioSpec().getChannelCount()
- == AudioSpec.CHANNEL_COUNT_NONE) {
- setAudioState(AudioState.UNSUPPORTED);
- } else {
- setAudioState(AudioState.INITIALIZING);
- }
synchronized (mLock) {
if (getObservableData(mState) == State.RESETTING) {
resetInternal();
@@ -1379,6 +1372,11 @@
}
}
+ boolean isAudioSupported() {
+ return getObservableData(mMediaSpec).getAudioSpec().getChannelCount()
+ != AudioSpec.CHANNEL_COUNT_NONE;
+ }
+
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
void setState(@NonNull State state) {
synchronized (mLock) {
@@ -1395,6 +1393,7 @@
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @ExecutedBy("mSequentialExecutor")
void setAudioState(AudioState audioState) {
Logger.d(TAG, "Transitioning audio state: " + mAudioState + " --> " + audioState);
mAudioState = audioState;
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/MLKitBarcodeTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/MLKitBarcodeTest.kt
index 3536ad5..195d892 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/MLKitBarcodeTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/MLKitBarcodeTest.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.util.Log
import android.util.Size
+import android.view.Surface
import androidx.camera.camera2.Camera2Config
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraX
@@ -47,7 +48,11 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-// The integration-tests for MLKit vision barcode component with CameraX ImageAnalysis use case.
+/* The integration-test is for MLKit vision barcode component with CameraX ImageAnalysis use case.
+ The test is for lab device test. For the local test, mark the @LabTestRule.LabTestFrontCamera,
+ and @LabTestRule.LabTestRearCamera, or enable "setprop log.tag.rearCameraE2E DEBUG" or
+ "setprop log.tag.frontCameraE2E DEBUG" using 'adb shell'.
+*/
@LargeTest
@RunWith(Parameterized::class)
class MLKitBarcodeTest(
@@ -74,6 +79,9 @@
private lateinit var camera: CameraUseCaseAdapter
// For MK Kit Barcode scanner
private lateinit var barcodeScanner: BarcodeScanner
+ private var imageResolution: Size = resolution
+ private var imageRotation: Int = Surface.ROTATION_0
+ private var targetRotation: Int = Surface.ROTATION_0
@Before
fun setup() {
@@ -103,7 +111,7 @@
@LabTestRule.LabTestFrontCamera
@Test
fun barcodeDetectViaFontCamera() {
- val imageAnalysis = initImageAnalysis()
+ val imageAnalysis = initImageAnalysis(CameraSelector.LENS_FACING_FRONT)
camera = CameraUtil.createCameraAndAttachUseCase(
context,
@@ -116,7 +124,7 @@
@LabTestRule.LabTestRearCamera
@Test
fun barcodeDetectViaRearCamera() {
- val imageAnalysis = initImageAnalysis()
+ val imageAnalysis = initImageAnalysis(CameraSelector.LENS_FACING_BACK)
camera = CameraUtil.createCameraAndAttachUseCase(
context,
@@ -132,7 +140,8 @@
imageAnalysis.setAnalyzer(
CameraXExecutors.ioExecutor()
) { imageProxy ->
- Log.d(TAG, "Process image proxy: $imageProxy")
+ imageResolution = Size(imageProxy.image!!.width, imageProxy.image!!.height)
+ imageRotation = imageProxy.imageInfo.rotationDegrees
barcodeScanner.process(
InputImage.fromMediaImage(
imageProxy.image!!,
@@ -154,31 +163,29 @@
// received images when finished using them. Otherwise, new images may not be
// received or the camera may stall.
.addOnCompleteListener {
- Log.d(TAG, "Close image proxy: $imageProxy")
imageProxy.close()
}
}
// Verify it is the CameraX lab test environment and can detect qr-code.
assertWithMessage(
- "Fail to detect qrcode, resolution: $resolution, " +
- "rearCameraE2E: ${isLoggable(true)}, " +
- "frontCameraE2E: ${isLoggable(false)} "
+ "Fail to detect qrcode, target resolution: $resolution, " +
+ "image resolution: $imageResolution, " +
+ "target rotation: $targetRotation, " +
+ "image rotation: $imageRotation "
).that(latchForBarcodeDetect.await(DETECT_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
}
- private fun initImageAnalysis(): ImageAnalysis {
+ private fun initImageAnalysis(lensFacing: Int): ImageAnalysis {
+ val sensorOrientation = CameraUtil.getSensorOrientation(lensFacing)
+ val isRotateNeeded = sensorOrientation!! % 180 != 0
+ Log.d(TAG, "Sensor Orientation: $sensorOrientation, lensFacing: $lensFacing")
+ targetRotation = if (isRotateNeeded) Surface.ROTATION_90 else Surface.ROTATION_0
+
return ImageAnalysis.Builder()
.setTargetName("ImageAnalysis")
.setTargetResolution(resolution)
+ .setTargetRotation(targetRotation)
.build()
}
-
- private fun isLoggable(isRear: Boolean): Boolean {
- return if (isRear) {
- Log.isLoggable("rearCameraE2E", Log.DEBUG)
- } else {
- Log.isLoggable("frontCameraE2E", Log.DEBUG)
- }
- }
}
diff --git a/car/app/app-automotive/src/main/aidl/androidx/car/app/activity/renderer/IRendererService.aidl b/car/app/app-automotive/src/main/aidl/androidx/car/app/activity/renderer/IRendererService.aidl
index ecbf0de..73374bf 100644
--- a/car/app/app-automotive/src/main/aidl/androidx/car/app/activity/renderer/IRendererService.aidl
+++ b/car/app/app-automotive/src/main/aidl/androidx/car/app/activity/renderer/IRendererService.aidl
@@ -19,6 +19,7 @@
import android.content.ComponentName;
import android.content.Intent;
import androidx.car.app.activity.renderer.ICarAppActivity;
+import androidx.car.app.serialization.Bundleable;
/**
* An interface to be used for communicating with the renderer.
@@ -56,4 +57,14 @@
* @param serviceName the service that is associated with the activity
*/
void terminate(in ComponentName serviceName) = 3;
+
+ /**
+ * Performs a handshake, negotiating the api level for communication between app and host.
+ *
+ * @param serviceName the component for the car app service that the handshake is for
+ * @param appLatestApiLevel the latest api level for the app side
+ *
+ * @return a {@link HandshakeInfo} including the negotiated api level
+ */
+ Bundleable performHandshake(in ComponentName serviceName, int appLatestApiLevel) = 4;
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/LogTags.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/LogTags.java
index a3a38c4..b154bfb 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/LogTags.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/LogTags.java
@@ -29,6 +29,8 @@
public final class LogTags {
public static final String TAG = androidx.car.app.utils.LogTags.TAG + ".Act";
+ public static final String TAG_ERROR = androidx.car.app.utils.LogTags.TAG + ".Error";
+
private LogTags() {
}
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
index 0022de4..4eaa1d2 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceConnectionManager.java
@@ -35,8 +35,10 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
+import androidx.car.app.HandshakeInfo;
import androidx.car.app.activity.renderer.ICarAppActivity;
import androidx.car.app.activity.renderer.IRendererService;
+import androidx.car.app.versioning.CarAppApiLevels;
import java.util.List;
@@ -59,10 +61,15 @@
private final Context mContext;
private final ServiceDispatcher mServiceDispatcher;
private int mDisplayId;
- @Nullable private Intent mIntent;
- @Nullable private ICarAppActivity mICarAppActivity;
+ @Nullable
+ private Intent mIntent;
+ @Nullable
+ private ICarAppActivity mICarAppActivity;
+ @Nullable
+ private HandshakeInfo mHandshakeInfo;
- @Nullable IRendererService mRendererService;
+ @Nullable
+ IRendererService mRendererService;
/** A listener receive connection status updates */
public interface ServiceConnectionListener extends ErrorHandler {
@@ -83,7 +90,8 @@
* Returns a {@link ServiceDispatcher} that can be used to communicate with the renderer
* service.
*/
- @NonNull ServiceDispatcher getServiceDispatcher() {
+ @NonNull
+ ServiceDispatcher getServiceDispatcher() {
return mServiceDispatcher;
}
@@ -107,6 +115,14 @@
mRendererService = rendererService;
}
+ /**
+ * Returns the {@link HandshakeInfo} that has been agreed with the host.
+ */
+ @Nullable
+ public HandshakeInfo getHandshakeInfo() {
+ return mHandshakeInfo;
+ }
+
/** Returns true if the service is currently bound and able to receive messages */
boolean isBound() {
return mRendererService != null;
@@ -158,7 +174,7 @@
// Host rejected the binding.
Log.i(LogTags.TAG, "Host service " + name + " rejected the binding "
- + "request");
+ + "request");
mListener.onError(ErrorHandler.ErrorType.HOST_INCOMPATIBLE);
}
};
@@ -236,6 +252,13 @@
IRendererService rendererService = requireNonNull(mRendererService);
ComponentName serviceComponentName = requireNonNull(mServiceComponentName);
+ // If the host does not support the getHandshakeInfo API, return oldest as it means to
+ // communicate at minimum level.
+ mHandshakeInfo = mServiceDispatcher.fetchNoFail("performHandshake",
+ new HandshakeInfo("", CarAppApiLevels.getOldest()),
+ () -> (HandshakeInfo) rendererService.performHandshake(serviceComponentName,
+ CarAppApiLevels.getLatest()).get());
+
Boolean success = mServiceDispatcher.fetch("initialize", false,
() -> rendererService.initialize(carAppActivity,
serviceComponentName, mDisplayId));
@@ -244,6 +267,7 @@
mListener.onError(ErrorHandler.ErrorType.HOST_ERROR);
return;
}
+
if (!updateIntent()) {
return;
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceDispatcher.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceDispatcher.java
index 0ee6769..8f2a450 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceDispatcher.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ServiceDispatcher.java
@@ -85,12 +85,15 @@
}
/**
- * Retrieves a value from the service. This is a blocking call.
+ * Retrieves a value from the service handling any communication error and displaying the
+ * error to the user.
*
- * @param call code to execute to retrieve the value
+ * <p>This is a blocking call
+ *
+ * @param description name for logging purposes
* @param fallbackValue value to return in case the call is unsuccessful
- * @param <T> type of value to retrieve
- * @return the value retrieved
+ * @param call code to execute to retrieve the value
+ * @return the value retrieved or the {@code fallbackValue} if the call failed
*/
// TODO(b/184697399): Remove two-way calls as these are blocking.
@Nullable
@@ -102,8 +105,7 @@
}
try {
// TODO(b/184697267): Implement ANR (application not responding) checks
- T value = call.invoke();
- return value;
+ return call.invoke();
} catch (DeadObjectException e) {
Log.e(LogTags.TAG, "Connection lost", e);
mErrorHandler.onError(ErrorHandler.ErrorType.HOST_CONNECTION_LOST);
@@ -119,4 +121,35 @@
}
return fallbackValue;
}
+
+ /**
+ * Retrieves a value from the service, ignoring any communication error and just returning
+ * the {@code fallbackValue} if an error is encountered in the communication.
+ *
+ * <p>This is a blocking call
+ *
+ * @param description name for logging purposes
+ * @param fallbackValue value to return in case the call is unsuccessful
+ * @param call code to execute to retrieve the value
+ * @return the value retrieved or the {@code fallbackValue} if the call failed
+ */
+ @Nullable
+ public <T> T fetchNoFail(@NonNull String description, @Nullable T fallbackValue,
+ @NonNull ReturnCall<T> call) {
+ if (!mOnBindingListener.isBound()) {
+ // Avoid dispatching messages if we are not bound to the service
+ return fallbackValue;
+ }
+ try {
+ // TODO(b/184697267): Implement ANR (application not responding) checks
+ return call.invoke();
+ } catch (RemoteException e) {
+ Log.e(LogTags.TAG, "Remote exception (host render service)", e);
+ } catch (BundlerException e) {
+ Log.e(LogTags.TAG, "Bundler exception (protocol)", e);
+ } catch (RuntimeException e) {
+ Log.e(LogTags.TAG, "Runtime exception (unknown)", e);
+ }
+ return fallbackValue;
+ }
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
index ce4389b..7c6e9e7 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/activity/ui/ErrorMessageView.java
@@ -17,13 +17,17 @@
package androidx.car.app.activity.ui;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.activity.ErrorHandler.ActionType.UPDATE_HOST;
+import static androidx.car.app.activity.LogTags.TAG_ERROR;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
@@ -49,11 +53,15 @@
*/
@RestrictTo(LIBRARY)
public final class ErrorMessageView extends LinearLayout {
+
private static final String VENDING_PACKAGE = "com.android.vending";
private static final String VENDING_DETAIL_URL =
"https://play.google.com/store/apps/details?id=";
private static final String ACTION_RENDER = "android.car.template.host.RendererService";
+ // TODO(b/194324567): Remove the hard coded Google templates host package name
+ private static final String HOST_PACKAGE = "com.google.android.apps.automotive.templates.host";
+
private TextView mErrorMessage;
private Button mActionButton;
@Nullable
@@ -85,7 +93,9 @@
mActionButton.setOnClickListener(v -> onClick());
}
- /** Updates the error displayed by this view */
+ /**
+ * Updates the error displayed by this view
+ */
public void setError(@Nullable ErrorHandler.ErrorType errorType) {
mErrorType = errorType;
mErrorMessage.setText(mErrorType != null
@@ -95,6 +105,13 @@
? getContext().getString(mErrorType.getActionType().getActionResId())
: null);
mActionButton.setVisibility(mErrorType != null ? View.VISIBLE : View.GONE);
+
+ // If the vending app is not installed, hide the button and update the message.
+ if (mErrorType != null && mErrorType.getActionType() == UPDATE_HOST
+ && !isVendingPackageInstalled()) {
+ mActionButton.setVisibility(INVISIBLE);
+ mErrorMessage.setText(R.string.error_message_no_vending);
+ }
}
private void onClick() {
@@ -118,27 +135,46 @@
throw new IllegalArgumentException("Unknown action type: " + mErrorType.getActionType());
}
+ private boolean isVendingPackageInstalled() {
+ try {
+ requireActivity().getPackageManager().getPackageInfo(VENDING_PACKAGE, 0);
+ } catch (NameNotFoundException e) {
+ Log.d(TAG_ERROR, "The vending app not found");
+ return false;
+ }
+ return true;
+ }
+
private Intent getVendingIntent() {
Intent rendererIntent = new Intent(ACTION_RENDER);
List<ResolveInfo> resolveInfoList =
requireActivity().getPackageManager().queryIntentServices(
- rendererIntent,
- PackageManager.GET_META_DATA
- );
- // Redirect to the PlayStore package detail if only one package that handles
- // ACTION_RENDER is found. if found multiple or none, redirect to the PlayStore main
- // page.
+ rendererIntent,
+ PackageManager.GET_META_DATA
+ );
+ // Redirect to the vending app package detail if only one package that handles
+ // ACTION_RENDER is found.
+ // Redirect to GAS host page if found no package that handles ACTION_RENDER.
+ // if found multiple or none, redirect to the vending app main page.
if (resolveInfoList.size() == 1) {
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.setPackage(VENDING_PACKAGE);
- intent.setData(Uri.parse(VENDING_DETAIL_URL
- + resolveInfoList.get(0).serviceInfo.packageName));
- return intent;
+ Log.d(TAG_ERROR, "Find a host, redirect to the page for this host.");
+ return getHostPageIntent(resolveInfoList.get(0).serviceInfo.packageName);
+ } else if (resolveInfoList.size() == 0) {
+ Log.d(TAG_ERROR, "No host found on the device, redirect to GAS host page");
+ return getHostPageIntent(HOST_PACKAGE);
} else {
+ Log.d(TAG_ERROR, "Multiple host found, redirect to the vending app main page");
return requireActivity().getPackageManager().getLaunchIntentForPackage(VENDING_PACKAGE);
}
}
+ private Intent getHostPageIntent(String packageName) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setPackage(VENDING_PACKAGE);
+ intent.setData(Uri.parse(VENDING_DETAIL_URL + packageName));
+ return intent;
+ }
+
private FragmentActivity requireActivity() {
return (FragmentActivity) getContext();
}
diff --git a/car/app/app-automotive/src/main/res/values/strings.xml b/car/app/app-automotive/src/main/res/values/strings.xml
index ed4a207..37da928 100644
--- a/car/app/app-automotive/src/main/res/values/strings.xml
+++ b/car/app/app-automotive/src/main/res/values/strings.xml
@@ -39,4 +39,6 @@
<string name="error_message_multiple_hosts">Incompatible system</string>
<!-- Message displayed when an unknown error is detected [CHAR_LIMIT=200] -->
<string name="error_message_unknown_error">Unknown error</string>
+ <!-- Message displayed when an unknown error is detected [CHAR_LIMIT=200] -->
+ <string name="error_message_no_vending">Please contact car services</string>
</resources>
\ No newline at end of file
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
index a633647..7e75ece 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/CarAppActivityTest.java
@@ -345,37 +345,4 @@
});
}
}
-
- // Use delegate to forward events to a mock. Mockito interceptor is not maintained on
- // top-level IBinder after call to IRenderService.Stub.asInterface() in CarAppActivity.
- private static class RenderServiceDelegate extends IRendererService.Stub {
- private final IRendererService mService;
- private ICarAppActivity mCarAppActivity;
-
- RenderServiceDelegate(IRendererService service) {
- mService = service;
- }
-
- @Override
- public boolean initialize(ICarAppActivity carActivity, ComponentName serviceName,
- int displayId) throws RemoteException {
- mCarAppActivity = carActivity;
- return mService.initialize(carActivity, serviceName, displayId);
- }
-
- @Override
- public boolean onNewIntent(Intent intent, ComponentName serviceName, int displayId)
- throws RemoteException {
- return mService.onNewIntent(intent, serviceName, displayId);
- }
-
- @Override
- public void terminate(ComponentName serviceName) throws RemoteException {
- mService.terminate(serviceName);
- }
-
- public ICarAppActivity getCarAppActivity() {
- return mCarAppActivity;
- }
- }
}
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/RenderServiceDelegate.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/RenderServiceDelegate.java
new file mode 100644
index 0000000..cccc414
--- /dev/null
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/RenderServiceDelegate.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.activity;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.RemoteException;
+
+import androidx.car.app.HandshakeInfo;
+import androidx.car.app.activity.renderer.ICarAppActivity;
+import androidx.car.app.activity.renderer.IRendererService;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.versioning.CarAppApiLevels;
+
+/**
+ * Use delegate to forward events to a mock. Mockito interceptor is not maintained on top-level
+ * IBinder after call to IRenderService.Stub.asInterface() in CarAppActivity.
+ */
+public class RenderServiceDelegate extends IRendererService.Stub {
+ private final IRendererService mService;
+ private ICarAppActivity mCarAppActivity;
+
+ RenderServiceDelegate(IRendererService service) {
+ mService = service;
+ }
+
+ @Override
+ public boolean initialize(ICarAppActivity carActivity, ComponentName serviceName,
+ int displayId) throws RemoteException {
+ mCarAppActivity = carActivity;
+ return mService.initialize(carActivity, serviceName, displayId);
+ }
+
+ @Override
+ public boolean onNewIntent(Intent intent, ComponentName serviceName, int displayId)
+ throws RemoteException {
+ return mService.onNewIntent(intent, serviceName, displayId);
+ }
+
+ @Override
+ public void terminate(ComponentName serviceName) throws RemoteException {
+ mService.terminate(serviceName);
+ }
+
+ @Override
+ public Bundleable performHandshake(ComponentName serviceName, int appLatestApiLevel)
+ throws RemoteException {
+ mService.performHandshake(serviceName, appLatestApiLevel);
+ try {
+ return Bundleable.create(new HandshakeInfo("", CarAppApiLevels.getLatest()));
+ } catch (BundlerException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /** Returns the {@link ICarAppActivity} received in {@link #initialize}. */
+ public ICarAppActivity getCarAppActivity() {
+ return mCarAppActivity;
+ }
+}
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
index 1b7373d..5bfc022 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/activity/ServiceConnectionManagerTest.java
@@ -37,8 +37,12 @@
import android.os.RemoteException;
import android.util.Log;
+import androidx.car.app.HandshakeInfo;
import androidx.car.app.activity.renderer.ICarAppActivity;
import androidx.car.app.activity.renderer.IRendererService;
+import androidx.car.app.serialization.Bundleable;
+import androidx.car.app.serialization.BundlerException;
+import androidx.car.app.versioning.CarAppApiLevels;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Test;
@@ -95,6 +99,8 @@
anyInt())).thenReturn(true);
when(mRenderService.onNewIntent(any(Intent.class), any(ComponentName.class),
anyInt())).thenReturn(true);
+ when(mRenderService.performHandshake(any(ComponentName.class), anyInt())).thenReturn(
+ Bundleable.create(new HandshakeInfo("", CarAppApiLevels.getLatest())));
ShadowApplication sa = shadowOf(app);
sa.setComponentNameAndServiceForBindService(mRendererComponent, mRenderServiceDelegate);
@@ -188,9 +194,43 @@
TEST_DISPLAY_ID);
verify(renderService).onNewIntent(TEST_INTENT, mFakeCarAppServiceComponent,
TEST_DISPLAY_ID);
+ verify(renderService).performHandshake(mFakeCarAppServiceComponent,
+ CarAppApiLevels.getLatest());
} catch (RemoteException e) {
fail(Log.getStackTraceString(e));
}
+ assertThat(mServiceConnectionManager.getHandshakeInfo().getHostCarAppApiLevel()).isEqualTo(
+ CarAppApiLevels.getLatest());
+ assertThat(mServiceConnectionManager.isBound()).isTrue();
+ }
+
+ @Test
+ public void testBind_hostDoesNotSupportHandshake_usesMinLevel() throws RemoteException {
+ setupCarAppActivityForTesting();
+ ICarAppActivity iCarAppActivity = mock(ICarAppActivity.class);
+
+ IRendererService renderService = createMockRendererService();
+ when(renderService.performHandshake(any(ComponentName.class), anyInt())).thenThrow(
+ new SecurityException());
+
+ mServiceConnectionManager.setRendererService(renderService);
+ mServiceConnectionManager.bind(TEST_INTENT, iCarAppActivity, TEST_DISPLAY_ID);
+ mMainLooper.idle();
+
+ try {
+ assertThat(mViewModel.getState().getValue()).isEqualTo(CarAppViewModel.State.CONNECTED);
+ assertThat(mViewModel.getError().getValue()).isNull();
+ verify(renderService).initialize(iCarAppActivity, mFakeCarAppServiceComponent,
+ TEST_DISPLAY_ID);
+ verify(renderService).onNewIntent(TEST_INTENT, mFakeCarAppServiceComponent,
+ TEST_DISPLAY_ID);
+ verify(renderService).performHandshake(mFakeCarAppServiceComponent,
+ CarAppApiLevels.getLatest());
+ } catch (RemoteException e) {
+ fail(Log.getStackTraceString(e));
+ }
+ assertThat(mServiceConnectionManager.getHandshakeInfo().getHostCarAppApiLevel()).isEqualTo(
+ CarAppApiLevels.getOldest());
assertThat(mServiceConnectionManager.isBound()).isTrue();
}
@@ -270,33 +310,6 @@
assertThat(mServiceConnectionManager.isBound()).isFalse();
}
- // Use delegate to forward events to a mock. Mockito interceptor is not maintained on
- // top-level IBinder after call to IRenderService.Stub.asInterface() in CarAppActivity.
- private static class RenderServiceDelegate extends IRendererService.Stub {
- private final IRendererService mService;
-
- RenderServiceDelegate(IRendererService service) {
- mService = service;
- }
-
- @Override
- public boolean initialize(ICarAppActivity carActivity, ComponentName serviceName,
- int displayId) throws RemoteException {
- return mService.initialize(carActivity, serviceName, displayId);
- }
-
- @Override
- public boolean onNewIntent(Intent intent, ComponentName serviceName, int displayId)
- throws RemoteException {
- return mService.onNewIntent(intent, serviceName, displayId);
- }
-
- @Override
- public void terminate(ComponentName serviceName) throws RemoteException {
- mService.terminate(serviceName);
- }
- }
-
private IRendererService createMockRendererService() {
IRendererService renderService = mock(IRendererService.class);
try {
@@ -305,7 +318,9 @@
anyInt())).thenReturn(true);
when(renderService.onNewIntent(any(Intent.class), any(ComponentName.class),
anyInt())).thenReturn(true);
- } catch (RemoteException e) {
+ when(renderService.performHandshake(any(ComponentName.class), anyInt())).thenReturn(
+ Bundleable.create(new HandshakeInfo("", CarAppApiLevels.getLatest())));
+ } catch (RemoteException | BundlerException e) {
fail(Log.getStackTraceString(e));
}
return renderService;
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 63cdf9e..44a0afd 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -40,9 +40,9 @@
api("androidx.annotation:annotation:1.1.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui:1.0.0")
+ implementation("androidx.compose.ui:ui-unit:1.0.0")
+ implementation("androidx.compose.ui:ui-util:1.0.0")
implementation(libs.kotlinStdlib)
api(libs.kotlinCoroutinesCore)
@@ -57,7 +57,7 @@
androidTestImplementation(libs.testCore)
androidTestImplementation(libs.junit)
androidTestImplementation(project(":compose:animation:animation"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
androidTestImplementation(project(":compose:test-utils"))
lintPublish project(":compose:animation:animation-core-lint")
diff --git a/compose/animation/animation-core/samples/build.gradle b/compose/animation/animation-core/samples/build.gradle
index 18ad38d..d69fec1 100644
--- a/compose/animation/animation-core/samples/build.gradle
+++ b/compose/animation/animation-core/samples/build.gradle
@@ -32,10 +32,10 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(project(":compose:animation:animation-core"))
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-unit"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.ui:ui:1.0.0")
+ implementation("androidx.compose.ui:ui-unit:1.0.0")
+ implementation("androidx.compose.foundation:foundation:1.0.0")
+ implementation("androidx.compose.material:material:1.0.0")
}
androidx {
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index c222c21..b102bdf 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -38,12 +38,12 @@
api("androidx.annotation:annotation:1.1.0")
api(project(":compose:animation:animation"))
- api(project(":compose:foundation:foundation-layout"))
+ api("androidx.compose.foundation:foundation-layout:1.0.0")
api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-geometry"))
+ api("androidx.compose.ui:ui-geometry:1.0.0")
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.0.0")
implementation(libs.kotlinStdlibCommon)
implementation("androidx.core:core-ktx:1.5.0")
@@ -51,8 +51,8 @@
testImplementation(libs.testRunner)
testImplementation(libs.junit)
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation("androidx.compose.foundation:foundation:1.0.0")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
diff --git a/compose/animation/animation-graphics/samples/build.gradle b/compose/animation/animation-graphics/samples/build.gradle
index 6f2593e..8fd9347 100644
--- a/compose/animation/animation-graphics/samples/build.gradle
+++ b/compose/animation/animation-graphics/samples/build.gradle
@@ -33,10 +33,10 @@
implementation(project(":compose:animation:animation"))
implementation(project(":compose:animation:animation-graphics"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.foundation:foundation:1.0.0")
+ implementation("androidx.compose.material:material:1.0.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui-text"))
+ implementation("androidx.compose.ui:ui-text:1.0.0")
}
androidx {
diff --git a/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt
new file mode 100644
index 0000000..8e3a241
--- /dev/null
+++ b/compose/animation/animation-graphics/src/androidAndroidTest/kotlin/androidx/compose/animation/graphics/vector/AnimatorTest.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.animation.graphics.vector
+
+import androidx.compose.animation.core.InternalAnimationApi
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.graphics.vector.VectorProperty
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class AnimatorTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ private val tolerance = 0.01f
+
+ private fun objectAnimator(
+ propertyName: String,
+ duration: Int,
+ keyframes: List<Keyframe<Float>>,
+ startDelay: Int = 0,
+ repeatCount: Int = 0,
+ repeatMode: RepeatMode = RepeatMode.Restart,
+ ): ObjectAnimator {
+ return ObjectAnimator(
+ duration,
+ startDelay,
+ repeatCount,
+ repeatMode,
+ listOf(PropertyValuesHolderFloat(propertyName, keyframes))
+ )
+ }
+
+ private fun sequentialAnimatorSet(
+ propertyName: String,
+ startValue: Float,
+ durations: List<Int>,
+ toValues: List<Float>,
+ startDelay: Int = 0,
+ repeatCount: Int = 0,
+ repeatMode: RepeatMode = RepeatMode.Restart,
+ ): AnimatorSet {
+ if (durations.size != toValues.size) {
+ throw RuntimeException()
+ }
+ val size = durations.size
+ return AnimatorSet(
+ (0 until size).map { i ->
+ objectAnimator(
+ propertyName,
+ durations[i],
+ listOf(
+ Keyframe(0f, if (i == 0) startValue else toValues[i - 1], LinearEasing),
+ Keyframe(1f, toValues[i], LinearEasing),
+ ),
+ startDelay,
+ repeatCount,
+ repeatMode
+ )
+ },
+ Ordering.Sequentially
+ )
+ }
+
+ @Test
+ fun simpleFloatProperty() {
+ verifyAnimator(
+ objectAnimator(
+ "translateX",
+ 1000,
+ listOf(
+ Keyframe(0f, 0f, LinearEasing),
+ Keyframe(1f, 1000f, LinearEasing)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun keyframes() {
+ verifyAnimator(
+ objectAnimator(
+ "translateX",
+ 1000,
+ listOf(
+ Keyframe(0f, 0f, LinearEasing),
+ Keyframe(0.2f, 200f, LinearEasing),
+ Keyframe(0.5f, 500f, LinearEasing),
+ Keyframe(0.7f, 700f, LinearEasing),
+ Keyframe(1f, 1000f, LinearEasing)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun sequentialAnimatorSet() {
+ verifyAnimator(
+ sequentialAnimatorSet(
+ "translateX",
+ 0f,
+ listOf(200, 300, 200, 300),
+ listOf(200f, 500f, 700f, 1000f)
+ )
+ )
+ }
+
+ @OptIn(InternalAnimationApi::class, ExperimentalComposeUiApi::class)
+ private fun verifyAnimator(a: Animator) {
+ val isAtEnd = mutableStateOf(false)
+ val config = StateVectorConfig()
+ rule.setContent {
+ val transition = updateTransition(isAtEnd.value, label = "translateX")
+ a.Configure(transition, config, 1000, 0)
+ if (transition.isRunning) {
+ assertThat(config.getOrDefault(VectorProperty.TranslateX, -1f))
+ .isWithin(tolerance)
+ .of(
+ if (transition.targetState) {
+ transition.playTimeNanos / 1000f / 1000f
+ } else {
+ 1000f - transition.playTimeNanos / 1000f / 1000f
+ }
+ )
+ }
+ }
+ assertThat(config.getOrDefault(VectorProperty.TranslateX, -1f))
+ .isWithin(tolerance)
+ .of(0f)
+ // Start to end
+ rule.runOnIdle { isAtEnd.value = true }
+ rule.waitForIdle()
+ assertThat(config.getOrDefault(VectorProperty.TranslateX, -1f))
+ .isWithin(tolerance)
+ .of(1000f)
+ // End to start
+ rule.runOnIdle { isAtEnd.value = false }
+ rule.waitForIdle()
+ assertThat(config.getOrDefault(VectorProperty.TranslateX, -1f))
+ .isWithin(tolerance)
+ .of(0f)
+ }
+}
diff --git a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt
index 5eb3680..846fe231c 100644
--- a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/vector/Animator.kt
@@ -118,13 +118,100 @@
}
Ordering.Sequentially -> {
var accumulatedDelay = parentDelay
- animators.fastForEach { animator ->
+ normalizeSequentialAnimators().fastForEach { animator ->
animator.Configure(transition, config, overallDuration, accumulatedDelay)
accumulatedDelay += animator.totalDuration
}
}
}
}
+
+ /**
+ * Normalizes a sequential animator set that is used as a list of keyframes for a single
+ * property. The animators are expected to run one after another animating the same property
+ * continuously, and none of the animators should use intermediate keyframes in it. If the
+ * animators meet this criteria, they are converted to an animator with multiple keyframes.
+ * Otherwise, this returns [animators] as they are.
+ */
+ private fun normalizeSequentialAnimators(): List<Animator> {
+ if (ordering != Ordering.Sequentially) {
+ return animators
+ }
+ var propertyName: String? = null
+ val keyframes = mutableListOf<Keyframe<Any?>>()
+ var startDelay: Int? = null
+ var resultHolder: PropertyValuesHolder<*>? = null
+ var accumulatedDuration = 0f
+ val totalDuration = totalDuration
+ for (animator in animators) {
+ if (animator !is ObjectAnimator) {
+ return animators
+ }
+ if (startDelay == null) {
+ startDelay = animator.startDelay
+ }
+ val holders = animator.holders
+ if (holders.size != 1) {
+ return animators
+ }
+ val holder = holders[0]
+ if (holder !is PropertyValuesHolder1D) {
+ return animators
+ }
+ if (propertyName == null) {
+ propertyName = holder.propertyName
+ } else if (propertyName != holder.propertyName) {
+ return animators
+ }
+ if (resultHolder == null) {
+ @Suppress("UNCHECKED_CAST")
+ resultHolder = when (holder) {
+ is PropertyValuesHolderFloat -> PropertyValuesHolderFloat(
+ propertyName,
+ keyframes as List<Keyframe<Float>>
+ )
+ is PropertyValuesHolderInt -> PropertyValuesHolderInt(
+ propertyName,
+ keyframes as List<Keyframe<Int>>
+ )
+ is PropertyValuesHolderPath -> PropertyValuesHolderPath(
+ propertyName,
+ keyframes as List<Keyframe<List<PathNode>>>
+ )
+ is PropertyValuesHolderColor -> PropertyValuesHolderColor(
+ propertyName,
+ keyframes as List<Keyframe<Color>>
+ )
+ }
+ }
+ if (holder.animatorKeyframes.size != 2) {
+ return animators
+ }
+ val start = holder.animatorKeyframes[0]
+ val end = holder.animatorKeyframes[1]
+ if (start.fraction != 0f || end.fraction != 1f) {
+ return animators
+ }
+ if (keyframes.isEmpty()) {
+ keyframes.add(Keyframe(0f, start.value, start.interpolator))
+ }
+ accumulatedDuration += animator.duration
+ val fraction = accumulatedDuration / (totalDuration - startDelay)
+ keyframes.add(Keyframe(fraction, end.value, end.interpolator))
+ }
+ if (resultHolder == null) {
+ return animators
+ }
+ return listOf(
+ ObjectAnimator(
+ duration = totalDuration,
+ startDelay = startDelay ?: 0,
+ repeatCount = 0,
+ repeatMode = RepeatMode.Restart,
+ holders = listOf(resultHolder)
+ )
+ )
+ }
}
internal sealed class PropertyValuesHolder<T> {
@@ -335,7 +422,8 @@
}
fun interpolate(fraction: Float): List<PathNode> {
- val index = (animatorKeyframes.indexOfFirst { it.fraction > fraction } - 1).coerceAtLeast(0)
+ val index = (animatorKeyframes.indexOfFirst { it.fraction >= fraction } - 1)
+ .coerceAtLeast(0)
val easing = animatorKeyframes[index + 1].interpolator
val innerFraction = easing.transform(
(
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 728e201..b204e6e 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -38,20 +38,20 @@
api("androidx.annotation:annotation:1.1.0")
api(project(":compose:animation:animation-core"))
- api(project(":compose:foundation:foundation-layout"))
+ api("androidx.compose.foundation:foundation-layout:1.0.0")
api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-geometry"))
+ api("androidx.compose.ui:ui:1.0.0")
+ api("androidx.compose.ui:ui-geometry:1.0.0")
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.0.0")
implementation(libs.kotlinStdlibCommon)
testImplementation(libs.testRules)
testImplementation(libs.testRunner)
testImplementation(libs.junit)
- androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation("androidx.compose.foundation:foundation:1.0.0")
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(libs.testRules)
androidTestImplementation(libs.testRunner)
diff --git a/compose/animation/animation/samples/build.gradle b/compose/animation/animation/samples/build.gradle
index 2423c37..4ea7433 100644
--- a/compose/animation/animation/samples/build.gradle
+++ b/compose/animation/animation/samples/build.gradle
@@ -33,10 +33,10 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(project(":compose:animation:animation"))
- implementation(project(":compose:foundation:foundation"))
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.foundation:foundation:1.0.0")
+ implementation("androidx.compose.material:material:1.0.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui-text"))
+ implementation("androidx.compose.ui:ui-text:1.0.0")
}
androidx {
diff --git a/compose/desktop/desktop/build.gradle b/compose/desktop/desktop/build.gradle
index 4f2b011..a11093e 100644
--- a/compose/desktop/desktop/build.gradle
+++ b/compose/desktop/desktop/build.gradle
@@ -45,7 +45,7 @@
api(project(":compose:material:material"))
api(project(":compose:runtime:runtime"))
api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-tooling"))
+ api(project(":compose:ui:ui-tooling-preview"))
}
jvmMain.dependencies {
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index a393eff..d4801bf 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -36,10 +36,10 @@
*/
api("androidx.annotation:annotation:1.1.0")
- api(project(":compose:ui:ui"))
+ api("androidx.compose.ui:ui:1.0.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui-util"))
+ implementation("androidx.compose.ui:ui-util:1.0.0")
implementation(libs.kotlinStdlibCommon)
testImplementation(libs.testRules)
@@ -47,7 +47,7 @@
testImplementation(libs.junit)
androidTestImplementation(project(":compose:foundation:foundation"))
- androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+ androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
androidTestImplementation(project(":compose:test-utils"))
androidTestImplementation(project(":activity:activity-compose"))
androidTestImplementation(libs.testRules)
diff --git a/compose/foundation/foundation-layout/samples/build.gradle b/compose/foundation/foundation-layout/samples/build.gradle
index dc0aefa1..81ada2a 100644
--- a/compose/foundation/foundation-layout/samples/build.gradle
+++ b/compose/foundation/foundation-layout/samples/build.gradle
@@ -32,10 +32,10 @@
compileOnly(project(":annotation:annotation-sampled"))
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.material:material:1.0.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-text"))
+ implementation("androidx.compose.ui:ui:1.0.0")
+ implementation("androidx.compose.ui:ui-text:1.0.0")
}
androidx {
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 6f4feee..c74d86d 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,14 +36,15 @@
* corresponding block above
*/
api("androidx.annotation:annotation:1.1.0")
- api(project(':compose:animation:animation'))
+ api("androidx.compose.animation:animation:1.0.0")
api(project(':compose:runtime:runtime'))
- api(project(':compose:ui:ui'))
+ api("androidx.compose.ui:ui:1.0.0")
implementation(libs.kotlinStdlibCommon)
implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:ui:ui-text"))
- implementation(project(":compose:ui:ui-util"))
+ implementation(project(":compose:ui:ui-graphics"))
+ implementation("androidx.compose.ui:ui-text:1.0.0")
+ implementation("androidx.compose.ui:ui-util:1.0.0")
testImplementation(project(":compose:test-utils"))
testImplementation(libs.testRules)
diff --git a/compose/foundation/foundation/samples/build.gradle b/compose/foundation/foundation/samples/build.gradle
index 6a77d10..3ff4f3f 100644
--- a/compose/foundation/foundation/samples/build.gradle
+++ b/compose/foundation/foundation/samples/build.gradle
@@ -31,13 +31,13 @@
compileOnly(project(":annotation:annotation-sampled"))
- implementation(project(":compose:animation:animation"))
+ implementation("androidx.compose.animation:animation:1.0.0")
implementation(project(":compose:foundation:foundation"))
implementation(project(":compose:foundation:foundation-layout"))
- implementation(project(":compose:material:material"))
+ implementation("androidx.compose.material:material:1.0.0")
implementation(project(":compose:runtime:runtime"))
- implementation(project(":compose:ui:ui"))
- implementation(project(":compose:ui:ui-text"))
+ implementation("androidx.compose.ui:ui:1.0.0")
+ implementation("androidx.compose.ui:ui-text:1.0.0")
}
androidx {
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index f2350fd..dd7311c 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -29,6 +29,7 @@
implementation("androidx.recyclerview:recyclerview:1.2.1")
kotlinPlugin(project(":compose:compiler:compiler"))
+ implementation(project(":compose:animation:animation-graphics"))
implementation(project(":compose:foundation:foundation-layout"))
implementation(project(":compose:material:material"))
implementation(project(":compose:material:material-icons-extended"))
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/resources/Resources.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/resources/Resources.kt
index 9c0d71e..c66d0c3 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/resources/Resources.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/resources/Resources.kt
@@ -23,6 +23,8 @@
package androidx.compose.integration.docs.resources
+import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
+import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Divider
import androidx.compose.material.Icon
@@ -32,6 +34,9 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
@@ -124,6 +129,21 @@
)
}
+@OptIn(ExperimentalAnimationGraphicsApi::class)
+@Composable
+private fun ResourcesSnippet7() {
+ // Files in res/drawable folders. For example:
+ // - res/drawable/animated_vector.xml
+
+ // In your Compose code
+ val image = animatedVectorResource(id = R.drawable.animated_vector)
+ val atEnd by remember { mutableStateOf(false) }
+ Icon(
+ painter = image.painterFor(atEnd = atEnd),
+ contentDescription = null // decorative element
+ )
+}
+
@Composable
private fun ResourcesSnippet8() {
Icon(Icons.Rounded.Menu, contentDescription = "Localized description")
diff --git a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
index 4ba6f05..e9b517e 100644
--- a/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
+++ b/compose/integration-tests/docs-snippets/src/main/java/androidx/compose/integration/docs/testing/Testing.kt
@@ -87,7 +87,7 @@
}
private object TestingSnippet3 {
- // file: app/src/androidTest/java/com/package/MyComposeTest.kt
+ // file: app/src/androidTest/kotlin/com/package/MyComposeTest.kt
class MyComposeTest {
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 9a4a588..10bac0a 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -206,6 +206,8 @@
@androidx.compose.runtime.Immutable public abstract sealed class Brush {
method public abstract void applyTo(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+ method public long getIntrinsicSize();
+ property public long intrinsicSize;
field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
}
@@ -417,6 +419,7 @@
@androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final inline class Matrix {
@@ -646,6 +649,7 @@
@androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final class RectHelper_androidKt {
@@ -1177,6 +1181,15 @@
method public static androidx.compose.ui.graphics.painter.BitmapPainter BitmapPainter(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional int filterQuality);
}
+ public final class BrushPainter extends androidx.compose.ui.graphics.painter.Painter {
+ ctor public BrushPainter(androidx.compose.ui.graphics.Brush brush);
+ method public androidx.compose.ui.graphics.Brush getBrush();
+ method public long getIntrinsicSize();
+ method protected void onDraw(androidx.compose.ui.graphics.drawscope.DrawScope);
+ property public final androidx.compose.ui.graphics.Brush brush;
+ property public long intrinsicSize;
+ }
+
public final class ColorPainter extends androidx.compose.ui.graphics.painter.Painter {
ctor public ColorPainter(long color);
method public long getColor();
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 83d79a0..0aa058c 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -206,6 +206,8 @@
@androidx.compose.runtime.Immutable public abstract sealed class Brush {
method public abstract void applyTo(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+ method public long getIntrinsicSize();
+ property public long intrinsicSize;
field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
}
@@ -422,6 +424,7 @@
@androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final inline class Matrix {
@@ -651,6 +654,7 @@
@androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final class RectHelper_androidKt {
@@ -1182,6 +1186,15 @@
method public static androidx.compose.ui.graphics.painter.BitmapPainter BitmapPainter(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional int filterQuality);
}
+ public final class BrushPainter extends androidx.compose.ui.graphics.painter.Painter {
+ ctor public BrushPainter(androidx.compose.ui.graphics.Brush brush);
+ method public androidx.compose.ui.graphics.Brush getBrush();
+ method public long getIntrinsicSize();
+ method protected void onDraw(androidx.compose.ui.graphics.drawscope.DrawScope);
+ property public final androidx.compose.ui.graphics.Brush brush;
+ property public long intrinsicSize;
+ }
+
public final class ColorPainter extends androidx.compose.ui.graphics.painter.Painter {
ctor public ColorPainter(long color);
method public long getColor();
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index b0a82ea..6f43701 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -236,6 +236,8 @@
@androidx.compose.runtime.Immutable public abstract sealed class Brush {
method public abstract void applyTo(long size, androidx.compose.ui.graphics.Paint p, float alpha);
+ method public long getIntrinsicSize();
+ property public long intrinsicSize;
field public static final androidx.compose.ui.graphics.Brush.Companion Companion;
}
@@ -449,6 +451,7 @@
@androidx.compose.runtime.Immutable public final class LinearGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final inline class Matrix {
@@ -678,6 +681,7 @@
@androidx.compose.runtime.Immutable public final class RadialGradient extends androidx.compose.ui.graphics.ShaderBrush {
method public android.graphics.Shader createShader(long size);
+ property public long intrinsicSize;
}
public final class RectHelper_androidKt {
@@ -1233,6 +1237,15 @@
method public static androidx.compose.ui.graphics.painter.BitmapPainter BitmapPainter(androidx.compose.ui.graphics.ImageBitmap image, optional long srcOffset, optional long srcSize, optional int filterQuality);
}
+ public final class BrushPainter extends androidx.compose.ui.graphics.painter.Painter {
+ ctor public BrushPainter(androidx.compose.ui.graphics.Brush brush);
+ method public androidx.compose.ui.graphics.Brush getBrush();
+ method public long getIntrinsicSize();
+ method protected void onDraw(androidx.compose.ui.graphics.drawscope.DrawScope);
+ property public final androidx.compose.ui.graphics.Brush brush;
+ property public long intrinsicSize;
+ }
+
public final class ColorPainter extends androidx.compose.ui.graphics.painter.Painter {
ctor public ColorPainter(long color);
method public long getColor();
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index dd24cefd..786ed1f 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -120,6 +120,83 @@
assertEquals(Color.Blue, pixelMap[5, centerY - 5])
}
+ @Test
+ fun testLinearGradientIntrinsicSize() {
+ assertEquals(
+ Size(100f, 200f),
+ Brush.linearGradient(
+ listOf(Color.Red, Color.Blue),
+ start = Offset(200f, 100f),
+ end = Offset(300f, 300f)
+ ).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testLinearGradientNegativePosition() {
+ assertEquals(
+ Size(100f, 200f),
+ Brush.linearGradient(
+ listOf(Color.Red, Color.Blue),
+ start = Offset(200f, 100f),
+ end = Offset(100f, -100f)
+ ).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testLinearGradientInfiniteWidth() {
+ assertEquals(
+ Size(Float.NaN, 200f),
+ Brush.linearGradient(
+ listOf(Color.Red, Color.Blue),
+ start = Offset(Float.POSITIVE_INFINITY, 100f),
+ end = Offset(Float.POSITIVE_INFINITY, 300f)
+ ).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testLinearGradientInfiniteHeight() {
+ assertEquals(
+ Size(100f, Float.NaN),
+ Brush.linearGradient(
+ listOf(Color.Red, Color.Blue),
+ start = Offset(100f, 0f),
+ end = Offset(200f, Float.POSITIVE_INFINITY)
+ ).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testSweepGradientIntrinsicSize() {
+ // Sweep gradients do not have an intrinsic size as they sweep/fill the geometry they are
+ // drawn with
+ assertEquals(
+ Size.Unspecified,
+ Brush.sweepGradient(listOf(Color.Red, Color.Blue)).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testRadialGradientIntrinsicSize() {
+ assertEquals(
+ Size(100f, 100f),
+ Brush.radialGradient(
+ listOf(Color.Red, Color.Blue),
+ radius = 50f
+ ).intrinsicSize
+ )
+ }
+
+ @Test
+ fun testRadialGradientInfiniteSize() {
+ assertEquals(
+ Size.Unspecified,
+ Brush.radialGradient(listOf(Color.Red, Color.Blue)).intrinsicSize
+ )
+ }
+
private fun ImageBitmap.drawInto(
block: DrawScope.() -> Unit
) = CanvasDrawScope().draw(
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt
new file mode 100644
index 0000000..aed63bc
--- /dev/null
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/painter/BrushPainterTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.painter
+
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.graphics.toPixelMap
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BrushPainterTest {
+
+ private fun createImageBitmap(): ImageBitmap {
+ val image = ImageBitmap(100, 100)
+ Canvas(image).drawRect(
+ 0f,
+ 0f,
+ 100f,
+ 100f,
+ Paint().apply { color = Color.White }
+ )
+ return image
+ }
+
+ @Test
+ fun testBrushPainter() {
+ val brushPainter = BrushPainter(
+ Brush.verticalGradient(
+ 0.0f to Color.Red,
+ 0.5f to Color.Red,
+ 0.5f to Color.Blue,
+ 1.0f to Color.Blue
+ )
+ )
+ val image = createImageBitmap()
+ drawPainter(brushPainter, Canvas(image), Size(100f, 100f))
+ val pixelMap = image.toPixelMap()
+ assertEquals(Color.Red, pixelMap[0, 0])
+ assertEquals(Color.Red, pixelMap[99, 0])
+ assertEquals(Color.Red, pixelMap[0, 49])
+ assertEquals(Color.Red, pixelMap[99, 49])
+
+ assertEquals(Color.Blue, pixelMap[0, 50])
+ assertEquals(Color.Blue, pixelMap[99, 50])
+ assertEquals(Color.Blue, pixelMap[0, 99])
+ assertEquals(Color.Blue, pixelMap[99, 99])
+ }
+
+ @Test
+ fun testBrushPainterAlphaApplied() {
+ val brushPainter = BrushPainter(
+ Brush.verticalGradient(
+ 0.0f to Color.Red,
+ 0.5f to Color.Red,
+ 0.5f to Color.Blue,
+ 1.0f to Color.Blue
+ )
+ )
+ val image = createImageBitmap()
+ drawPainter(brushPainter, Canvas(image), Size(100f, 100f), alpha = 0.5f)
+
+ val expectedRed = Color(
+ alpha = 0.5f,
+ red = Color.Red.red,
+ green = 0f,
+ blue = 0f
+ ).compositeOver(Color.White)
+
+ val expectedBlue = Color(
+ alpha = 0.5f,
+ red = 0f,
+ green = 0f,
+ blue = Color.Blue.blue
+ ).compositeOver(Color.White)
+
+ val pixelMap = image.toPixelMap()
+ assertEquals(expectedRed, pixelMap[0, 0])
+ assertEquals(expectedRed, pixelMap[99, 0])
+ assertEquals(expectedRed, pixelMap[0, 49])
+ assertEquals(expectedRed, pixelMap[99, 49])
+
+ assertEquals(expectedBlue, pixelMap[0, 50])
+ assertEquals(expectedBlue, pixelMap[99, 50])
+ assertEquals(expectedBlue, pixelMap[0, 99])
+ assertEquals(expectedBlue, pixelMap[99, 99])
+ }
+
+ @Test
+ fun testBrushPainterTint() {
+ val brushPainter = BrushPainter(
+ Brush.verticalGradient(
+ 0.0f to Color.Red,
+ 0.5f to Color.Red,
+ 0.5f to Color.Blue,
+ 1.0f to Color.Blue
+ )
+ )
+ val image = createImageBitmap()
+ drawPainter(
+ brushPainter,
+ Canvas(image),
+ Size(100f, 100f),
+ colorFilter = ColorFilter.tint(Color.Cyan, BlendMode.SrcIn)
+ )
+ val pixelMap = image.toPixelMap()
+ assertEquals(Color.Cyan, pixelMap[0, 0])
+ assertEquals(Color.Cyan, pixelMap[99, 0])
+ assertEquals(Color.Cyan, pixelMap[0, 49])
+ assertEquals(Color.Cyan, pixelMap[99, 49])
+
+ assertEquals(Color.Cyan, pixelMap[0, 50])
+ assertEquals(Color.Cyan, pixelMap[99, 50])
+ assertEquals(Color.Cyan, pixelMap[0, 99])
+ assertEquals(Color.Cyan, pixelMap[99, 99])
+ }
+
+ @Test
+ fun testBrushPainterEquals() {
+ val brush1 = Brush.verticalGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Blue,
+ 0.7f to Color.Green
+ )
+
+ val brush2 = Brush.verticalGradient(
+ 0.0f to Color.Red,
+ 0.3f to Color.Blue,
+ 0.7f to Color.Green
+ )
+
+ assertEquals(BrushPainter(brush1), BrushPainter(brush2))
+ }
+
+ @Test
+ fun testBrushPainterHashCode() {
+ val brush = Brush.horizontalGradient(listOf(Color.Red, Color.Blue, Color.Yellow))
+ assertEquals(BrushPainter(brush).hashCode(), brush.hashCode())
+ }
+
+ @Test
+ fun testBrushPainterToString() {
+ val brush = Brush.verticalGradient(
+ listOf(Color.White, Color.Black, Color.Gray, Color.LightGray)
+ )
+ assertEquals("BrushPainter(brush=$brush)", BrushPainter(brush).toString())
+ }
+
+ @Test
+ fun testBrushPainterIntrinsicSize() {
+ val brush = Brush.verticalGradient(
+ listOf(Color.White, Color.Black),
+ startY = 0f,
+ endY = 100f
+ )
+ assertEquals(Size(0.0f, 100f), BrushPainter(brush).intrinsicSize)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index 35d0218..bda9615 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -24,10 +24,20 @@
import androidx.compose.ui.geometry.isFinite
import androidx.compose.ui.geometry.isSpecified
import androidx.compose.ui.geometry.isUnspecified
-import androidx.compose.ui.unit.center
+import kotlin.math.abs
@Immutable
sealed class Brush {
+
+ /**
+ * Return the intrinsic size of the [Brush].
+ * If the there is no intrinsic size (i.e. filling bounds with an arbitrary color) return
+ * [Size.Unspecified].
+ * If there is no intrinsic size in a single dimension, return [Size] with
+ * [Float.NaN] in the desired dimension.
+ */
+ open val intrinsicSize: Size = Size.Unspecified
+
abstract fun applyTo(size: Size, p: Paint, alpha: Float)
companion object {
@@ -434,6 +444,13 @@
private val tileMode: TileMode = TileMode.Clamp
) : ShaderBrush() {
+ override val intrinsicSize: Size
+ get() =
+ Size(
+ if (start.x.isFinite() && end.x.isFinite()) abs(start.x - end.x) else Float.NaN,
+ if (start.y.isFinite() && end.y.isFinite()) abs(start.y - end.y) else Float.NaN
+ )
+
override fun createShader(size: Size): Shader {
val startX = if (start.x == Float.POSITIVE_INFINITY) size.width else start.x
val startY = if (start.y == Float.POSITIVE_INFINITY) size.height else start.y
@@ -493,6 +510,9 @@
private val tileMode: TileMode = TileMode.Clamp
) : ShaderBrush() {
+ override val intrinsicSize: Size
+ get() = if (radius.isFinite()) Size(radius * 2, radius * 2) else Size.Unspecified
+
override fun createShader(size: Size): Shader {
val centerX: Float
val centerY: Float
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
index cf3527d..85217ee 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Shader.kt
@@ -112,6 +112,11 @@
colorStops: List<Float>?,
): Shader
+/**
+ * Creates a Shader using the given [ImageBitmap] as an input texture. If the shader is
+ * to be drawn in an area larger than the size of the [ImageBitmap], the region is filled
+ * in the horizontal and vertical directions based on the [tileModeX] and [tileModeY] parameters.
+ */
fun ImageShader(
image: ImageBitmap,
tileModeX: TileMode = TileMode.Clamp,
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/BrushPainter.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/BrushPainter.kt
new file mode 100644
index 0000000..cfd8419
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/BrushPainter.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.graphics.painter
+
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.drawscope.DrawScope
+
+/**
+ * [Painter] implementation used to fill the provided bounds with the specified [Brush].
+ * The intrinsic size of this [Painter] is determined by [Brush.intrinsicSize]
+ */
+class BrushPainter(
+ val brush: Brush,
+) : Painter() {
+
+ private var alpha: Float = 1.0f
+ private var colorFilter: ColorFilter? = null
+
+ override val intrinsicSize: Size
+ get() = brush.intrinsicSize
+
+ override fun DrawScope.onDraw() {
+ drawRect(brush = brush, alpha = alpha, colorFilter = colorFilter)
+ }
+
+ override fun applyAlpha(alpha: Float): Boolean {
+ this.alpha = alpha
+ return true
+ }
+
+ override fun applyColorFilter(colorFilter: ColorFilter?): Boolean {
+ this.colorFilter = colorFilter
+ return true
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is BrushPainter) return false
+
+ if (brush != other.brush) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return brush.hashCode()
+ }
+
+ override fun toString(): String {
+ return "BrushPainter(brush=$brush)"
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/Painter.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/Painter.kt
index 0149166..cad2d23 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/Painter.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/painter/Painter.kt
@@ -152,7 +152,7 @@
* If the there is no intrinsic size (i.e. filling bounds with an arbitrary color) return
* [Size.Unspecified].
* If there is no intrinsic size in a single dimension, return [Size] with
- * [Float.POSITIVE_INFINITY] in the desired dimension.
+ * [Float.NaN] in the desired dimension.
* If a [Painter] does not have an intrinsic size, it will always draw within the full
* bounds of the destination
*/
diff --git a/compose/ui/ui-inspection/build.gradle b/compose/ui/ui-inspection/build.gradle
index 014b40f..8b917d1 100644
--- a/compose/ui/ui-inspection/build.gradle
+++ b/compose/ui/ui-inspection/build.gradle
@@ -48,6 +48,7 @@
androidTestImplementation(libs.kotlinCoroutinesAndroid)
androidTestImplementation(project(":compose:ui:ui-tooling-data"))
androidTestImplementation(project(":compose:ui:ui"))
+ androidTestImplementation(project(":compose:ui:ui-util"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(project(":compose:material:material"))
androidTestImplementation(project(":inspection:inspection-testing"))
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
index 91087fb..e60d546 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/inspector/ParameterFactoryTest.kt
@@ -69,6 +69,7 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
+import androidx.compose.ui.util.packFloats
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
@@ -218,12 +219,21 @@
parameter("x", ParameterType.DimensionDp, 2.5f)
parameter("y", ParameterType.DimensionDp, 5.0f)
}
+ parameter("intrinsicSize", ParameterType.String, Size::class.java.simpleName) {
+ val width = 5.0f
+ val height = 9.5f
+ parameter("height", ParameterType.Float, height)
+ parameter("maxDimension", ParameterType.Float, height)
+ parameter("minDimension", ParameterType.Float, width)
+ parameter("packedValue", ParameterType.Int64, packFloats(width, height))
+ parameter("width", ParameterType.Float, width)
+ }
parameter("start", ParameterType.String, Offset::class.java.simpleName) {
parameter("x", ParameterType.DimensionDp, 0.0f)
parameter("y", ParameterType.DimensionDp, 0.25f)
}
- parameter("tileMode", ParameterType.String, "Clamp", index = 4)
- parameter("createdSize", ParameterType.String, "Unspecified", index = 5)
+ parameter("tileMode", ParameterType.String, "Clamp", index = 5)
+ parameter("createdSize", ParameterType.String, "Unspecified", index = 6)
}
}
// TODO: add tests for RadialGradient & ShaderBrush
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index 201c607..495790ae 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+
+import androidx.build.AndroidXComposePlugin
import androidx.build.LibraryGroups
import androidx.build.LibraryType
@@ -21,17 +23,43 @@
id("AndroidXPlugin")
id("AndroidXComposePlugin")
id("com.android.library")
- id("org.jetbrains.kotlin.android")
}
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+
+
dependencies {
- implementation(libs.kotlinStdlib)
- api("androidx.annotation:annotation:1.2.0")
- api(project(":compose:runtime:runtime"))
-
- testImplementation(libs.junit)
+ if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ implementation(libs.kotlinStdlib)
+ api("androidx.annotation:annotation:1.2.0")
+ api(project(":compose:runtime:runtime"))
+ testImplementation(libs.junit)
+ }
}
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+ kotlin {
+ android()
+ jvm("desktop")
+
+ /*
+ * When updating dependencies, make sure to make the an an analogous update in the
+ * corresponding block above
+ */
+ sourceSets {
+ commonMain.dependencies {
+ implementation(libs.kotlinStdlibCommon)
+ api("androidx.annotation:annotation:1.2.0")
+ api(project(":compose:runtime:runtime"))
+ }
+
+ androidMain.dependsOn(jvmMain)
+ desktopMain.dependsOn(jvmMain)
+ }
+ }
+}
+
+
androidx {
name = "Compose Tooling API"
type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-tooling-preview/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-preview/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/AndroidManifest.xml
rename to compose/ui/ui-tooling-preview/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/Device.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/Device.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Device.kt
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/Preview.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/Preview.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/Preview.kt
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/PreviewParameter.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/PreviewParameter.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/PreviewParameter.kt
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/UiMode.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/UiMode.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/UiMode.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/UiMode.kt
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/datasource/CollectionPreviewParameterProvider.kt
diff --git a/compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt b/compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
similarity index 100%
rename from compose/ui/ui-tooling-preview/src/main/java/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
rename to compose/ui/ui-tooling-preview/src/androidMain/kotlin/androidx/compose/ui/tooling/preview/datasource/LoremIpsum.kt
diff --git a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt b/compose/ui/ui-tooling-preview/src/desktopMain/androidx/compose/desktop/ui/tooling/preview/Preview.kt
similarity index 100%
rename from compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/preview/Preview.kt
rename to compose/ui/ui-tooling-preview/src/desktopMain/androidx/compose/desktop/ui/tooling/preview/Preview.kt
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index ff3f55b..a9e348e 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -71,14 +71,13 @@
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinStdlibCommon)
-
+ api(project(":compose:ui:ui-tooling-preview"))
+ api(project(":compose:runtime:runtime"))
+ api(project(":compose:ui:ui"))
}
androidMain.dependencies {
api("androidx.annotation:annotation:1.1.0")
- api(project(":compose:runtime:runtime"))
- api(project(":compose:ui:ui"))
- api(project(":compose:ui:ui-tooling-preview"))
api(project(":compose:ui:ui-tooling-data"))
implementation(project(":compose:material:material"))
implementation("androidx.activity:activity-compose:1.3.0")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 6358a0b..085a1f7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -24,6 +24,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
@@ -288,6 +289,36 @@
}
@Test
+ fun replacedChildren_includeFakeNodes() {
+ val tag = "tag1"
+ rule.setContent {
+ SimpleTestLayout(Modifier.clickable(role = Role.Button, onClick = {}).testTag(tag)) {
+ BasicText("text")
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag, true).fetchSemanticsNode()
+ val children = node.replacedChildren
+ assertThat(children.count()).isEqualTo(2)
+ assertThat(children.last().isFake).isTrue()
+ }
+
+ @Test
+ fun children_doNotIncludeFakeNodes() {
+ val tag = "tag1"
+ rule.setContent {
+ SimpleTestLayout(Modifier.clickable(role = Role.Button, onClick = {}).testTag(tag)) {
+ BasicText("text")
+ }
+ }
+
+ val node = rule.onNodeWithTag(tag, true).fetchSemanticsNode()
+ val children = node.children
+ assertThat(children.count()).isEqualTo(1)
+ assertThat(children.last().isFake).isFalse()
+ }
+
+ @Test
fun removingMergedSubtree_updatesSemantics() {
val label = "foo"
val showSubtree = mutableStateOf(true)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 328b44f..41b1f8b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -161,7 +161,7 @@
unmergedChildren().fastForEach { child ->
// Don't merge children that themselves merge all their descendants (because that
// indicates they're independently screen-reader-focusable).
- if (!child.isFake && !child.isMergingSemanticsOfDescendants) {
+ if (!child.isMergingSemanticsOfDescendants) {
mergedConfig.mergeChild(child.unmergedConfig)
child.mergeConfig(mergedConfig)
}
@@ -172,7 +172,10 @@
private val isMergingSemanticsOfDescendants: Boolean
get() = mergingEnabled && unmergedConfig.isMergingSemanticsOfDescendants
- internal fun unmergedChildren(sortByBounds: Boolean = false): List<SemanticsNode> {
+ internal fun unmergedChildren(
+ sortByBounds: Boolean = false,
+ includeFakeNodes: Boolean = false
+ ): List<SemanticsNode> {
if (this.isFake) return listOf()
val unmergedChildren: MutableList<SemanticsNode> = mutableListOf()
@@ -185,7 +188,9 @@
unmergedChildren.add(SemanticsNode(semanticsChild, mergingEnabled))
}
- emitFakeNodes(unmergedChildren)
+ if (includeFakeNodes) {
+ emitFakeNodes(unmergedChildren)
+ }
return unmergedChildren
}
@@ -199,7 +204,11 @@
// TODO(b/184376083): This is too expensive for a val (full subtree recreation every call);
// optimize this when the merging algorithm is improved.
val children: List<SemanticsNode>
- get() = getChildren(sortByBounds = false, includeReplacedSemantics = !mergingEnabled)
+ get() = getChildren(
+ sortByBounds = false,
+ includeReplacedSemantics = !mergingEnabled,
+ includeFakeNodes = false
+ )
/**
* Contains the children in inverse hit test order (i.e. paint order).
@@ -209,7 +218,11 @@
* This property is primarily used in Accessibility delegate.
*/
internal val replacedChildren: List<SemanticsNode>
- get() = getChildren(sortByBounds = false, includeReplacedSemantics = false)
+ get() = getChildren(
+ sortByBounds = false,
+ includeReplacedSemantics = false,
+ includeFakeNodes = true
+ )
/**
* Similar to [replacedChildren] but children are sorted by bounds: top to down, left to
@@ -218,11 +231,27 @@
// TODO(b/184376083): This is too expensive for a val (full subtree recreation every call);
// optimize this when the merging algorithm is improved.
internal val replacedChildrenSortedByBounds: List<SemanticsNode>
- get() = getChildren(sortByBounds = true, includeReplacedSemantics = false)
+ get() = getChildren(
+ sortByBounds = true,
+ includeReplacedSemantics = false,
+ includeFakeNodes = true
+ )
+ /**
+ * @param sortByBounds if true, nodes in the result list will be sorted with respect to their
+ * bounds. Otherwise children will be in the order they are added to the composition
+ * @param includeReplacedSemantics if true, the result will contain children of nodes marked
+ * as [clearAndSetSemantics]. For accessibility we always use false, but in testing and
+ * debugging we should be able to investigate both
+ * @param includeFakeNodes if true, the tree will include fake nodes. For accessibility we
+ * set to true, but for testing purposes we don't want to expose the fake nodes and therefore
+ * set to false. When Talkback can properly handle unmerged tree, fake nodes will be removed
+ * and so will be this parameter.
+ */
private fun getChildren(
sortByBounds: Boolean,
- includeReplacedSemantics: Boolean
+ includeReplacedSemantics: Boolean,
+ includeFakeNodes: Boolean
): List<SemanticsNode> {
if (!includeReplacedSemantics && unmergedConfig.isClearingSemantics) {
return listOf()
@@ -235,7 +264,7 @@
return findOneLayerOfMergingSemanticsNodes(sortByBounds = sortByBounds)
}
- return unmergedChildren(sortByBounds)
+ return unmergedChildren(sortByBounds, includeFakeNodes)
}
/**
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
index f27160b..ac41d30 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.desktop.kt
@@ -148,13 +148,17 @@
return list.lastOrNull()
}
+ private fun DesktopOwner?.isAbove(
+ targetOwner: DesktopOwner?
+ ) = list.indexOf(this) > list.indexOf(targetOwner)
+
fun onMousePressed(x: Int, y: Int, nativeEvent: MouseEvent? = null) {
isMousePressed = true
val currentOwner = hoveredOwner
if (currentOwner != null) {
- if (currentOwner.isFocusable && focusedOwner != currentOwner) {
+ if (focusedOwner.isAbove(currentOwner)) {
focusedOwner?.onDismissRequest?.invoke()
- focusedOwner = currentOwner
+ return
} else {
currentOwner.processPointerInput(
pointerInputEvent(nativeEvent, x, y, isMousePressed)
diff --git a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
index 97b77a7..63d6a76 100644
--- a/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
+++ b/core/core-google-shortcuts/src/androidTest/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImplTest.java
@@ -79,7 +79,6 @@
.setShortLabel("short label")
.setLongLabel("long label")
.setIntent(intent)
- .setIcon(IconCompat.createWithContentUri("content://abc"))
.build();
mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(shortcut));
@@ -221,7 +220,6 @@
.setShortLabel("short label")
.setLongLabel("long label")
.setIntent(intent)
- .setIcon(IconCompat.createWithContentUri("content://abc"))
.build();
mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
@@ -284,4 +282,58 @@
assertThat(actionsString).containsExactly(expectedAction1.toString(),
expectedAction2.toString());
}
+
+ @Test
+ @SmallTest
+ public void onShortcutUpdated_withUriIcon_savesToAppIndexWithUri() throws Exception {
+ ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+ Intent intent = Intent.parseUri("app://shortcut", 0);
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+ .setShortLabel("short label")
+ .setIntent(intent)
+ .setIcon(IconCompat.createWithContentUri(
+ "file:///data/user/0/com.example.myapp/files/ic_myicon.jpg"))
+ .build();
+
+ mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(shortcut));
+
+ verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+ List<Indexable> allValues = indexableCaptor.getAllValues();
+ Indexable expected = new ShortcutBuilder()
+ .setId("publicIntent")
+ .setShortcutLabel("short label")
+ .setUrl(ShortcutUtils.getIndexableUrl(mContext, "publicIntent"))
+ .setShortcutUrl(ShortcutUtils.getIndexableShortcutUrl(mContext, intent, null))
+ .setImage("file:///data/user/0/com.example.myapp/files/ic_myicon.jpg")
+ .build();
+ assertThat(allValues).containsExactly(expected);
+ }
+
+ @Test
+ @SmallTest
+ public void onShortcutUpdated_withAdaptiveUriIcon_savesToAppIndexWithUri() throws Exception {
+ ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+
+ Intent intent = Intent.parseUri("app://shortcut", 0);
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+ .setShortLabel("short label")
+ .setIntent(intent)
+ .setIcon(IconCompat.createWithAdaptiveBitmapContentUri(
+ "file:///data/user/0/com.example.myapp/files/ic_myicon.jpg"))
+ .build();
+
+ mShortcutInfoChangeListener.onShortcutUpdated(Collections.singletonList(shortcut));
+
+ verify(mFirebaseAppIndex, only()).update(indexableCaptor.capture());
+ List<Indexable> allValues = indexableCaptor.getAllValues();
+ Indexable expected = new ShortcutBuilder()
+ .setId("publicIntent")
+ .setShortcutLabel("short label")
+ .setUrl(ShortcutUtils.getIndexableUrl(mContext, "publicIntent"))
+ .setShortcutUrl(ShortcutUtils.getIndexableShortcutUrl(mContext, intent, null))
+ .setImage("file:///data/user/0/com.example.myapp/files/ic_myicon.jpg")
+ .build();
+ assertThat(allValues).containsExactly(expected);
+ }
}
diff --git a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
index 88e9a59..5c5afd7 100644
--- a/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
+++ b/core/core-google-shortcuts/src/main/java/androidx/core/google/shortcuts/ShortcutInfoChangeListenerImpl.java
@@ -34,6 +34,7 @@
import androidx.core.google.shortcuts.builders.CapabilityBuilder;
import androidx.core.google.shortcuts.builders.ParameterBuilder;
import androidx.core.google.shortcuts.builders.ShortcutBuilder;
+import androidx.core.graphics.drawable.IconCompat;
import com.google.crypto.tink.KeysetHandle;
import com.google.firebase.appindexing.Action;
@@ -191,6 +192,16 @@
}
}
+ // Add icon
+ if (shortcut.getIcon() != null) {
+ IconCompat icon = shortcut.getIcon();
+ if (icon.getType() == IconCompat.TYPE_URI_ADAPTIVE_BITMAP
+ || icon.getType() == IconCompat.TYPE_URI) {
+ // Assume the uri is public and can be opened by Google apps
+ shortcutBuilder.setImage(icon.getUri().toString());
+ }
+ }
+
// By default, the indexable will be saved only on-device.
return shortcutBuilder.build();
}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 2baef92..b6ca37c 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -256,6 +256,7 @@
docs(project(":wear:compose:compose-foundation"))
samples(project(":wear:compose:compose-foundation-samples"))
docs(project(":wear:compose:compose-material"))
+ samples(project(":wear:compose:compose-material-samples"))
docs(project(":wear:wear-input"))
docs(project(":wear:wear-input-testing"))
docs(project(":wear:wear-ongoing"))
diff --git a/hilt/hilt-navigation-compose/build.gradle b/hilt/hilt-navigation-compose/build.gradle
index 1989f53..078d991 100644
--- a/hilt/hilt-navigation-compose/build.gradle
+++ b/hilt/hilt-navigation-compose/build.gradle
@@ -35,12 +35,12 @@
}
dependencies {
- kotlinPlugin(project(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
api("androidx.hilt:hilt-navigation:1.0.0")
- api(project(":compose:runtime:runtime"))
- api("androidx.compose.ui:ui:1.0.0-rc02")
+ api("androidx.compose.runtime:runtime:1.0.1")
+ api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
api(projectOrArtifact(":navigation:navigation-compose"))
diff --git a/hilt/hilt-navigation-compose/samples/build.gradle b/hilt/hilt-navigation-compose/samples/build.gradle
index 3c00b2c..8eec1e3 100644
--- a/hilt/hilt-navigation-compose/samples/build.gradle
+++ b/hilt/hilt-navigation-compose/samples/build.gradle
@@ -27,7 +27,7 @@
}
dependencies {
- kotlinPlugin(project(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
compileOnly(projectOrArtifact(":annotation:annotation-sampled"))
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 6bf586b..5346124 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -27,11 +27,11 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
- api(projectOrArtifact(":compose:runtime:runtime"))
- api "androidx.compose.ui:ui:1.0.0-rc02"
+ api("androidx.compose.runtime:runtime:1.0.1")
+ api "androidx.compose.ui:ui:1.0.1"
implementation(libs.kotlinStdlib)
diff --git a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
index f4fcc19..70c9431 100644
--- a/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/samples/build.gradle
@@ -26,7 +26,7 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
implementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose")
}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInFragmentTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInFragmentTest.kt
index 1e7ebc0..a6ba005 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInFragmentTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInFragmentTest.kt
@@ -24,7 +24,6 @@
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModel
-import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -56,7 +55,6 @@
activity = activityTestRule.activity
}
- @FlakyTest(bugId = 190608770)
@Test
public fun viewModelCreatedInFragment() {
val fragment = TestFragment(viewModelClass)
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index faa0cd4..5ccc622 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -487,17 +487,14 @@
ctor public NavigatorState();
method public abstract androidx.navigation.NavBackStackEntry createBackStackEntry(androidx.navigation.NavDestination destination, android.os.Bundle? arguments);
method public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> getBackStack();
- method public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> getTransitionsInProgress();
+ method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
+ method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
+ method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
+ method public void pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
property public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> backStack;
- property public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> transitionsInProgress;
- }
-
- public static fun interface NavigatorState.OnTransitionCompleteListener {
- method public void onTransitionComplete();
+ property public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> transitionsInProgress;
}
@androidx.navigation.NavOptionsDsl public final class PopUpToBuilder {
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index faa0cd4..5ccc622 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -487,17 +487,14 @@
ctor public NavigatorState();
method public abstract androidx.navigation.NavBackStackEntry createBackStackEntry(androidx.navigation.NavDestination destination, android.os.Bundle? arguments);
method public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> getBackStack();
- method public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> getTransitionsInProgress();
+ method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
+ method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
+ method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
+ method public void pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
property public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> backStack;
- property public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> transitionsInProgress;
- }
-
- public static fun interface NavigatorState.OnTransitionCompleteListener {
- method public void onTransitionComplete();
+ property public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> transitionsInProgress;
}
@androidx.navigation.NavOptionsDsl public final class PopUpToBuilder {
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index faa0cd4..5ccc622 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -487,17 +487,14 @@
ctor public NavigatorState();
method public abstract androidx.navigation.NavBackStackEntry createBackStackEntry(androidx.navigation.NavDestination destination, android.os.Bundle? arguments);
method public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> getBackStack();
- method public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> getTransitionsInProgress();
+ method public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> getTransitionsInProgress();
+ method public void markTransitionComplete(androidx.navigation.NavBackStackEntry entry);
method public void pop(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
+ method public void popWithTransition(androidx.navigation.NavBackStackEntry popUpTo, boolean saveState);
method public void push(androidx.navigation.NavBackStackEntry backStackEntry);
- method public androidx.navigation.NavigatorState.OnTransitionCompleteListener pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
+ method public void pushWithTransition(androidx.navigation.NavBackStackEntry backStackEntry);
property public final kotlinx.coroutines.flow.StateFlow<java.util.List<androidx.navigation.NavBackStackEntry>> backStack;
- property public final kotlinx.coroutines.flow.StateFlow<java.util.Map<androidx.navigation.NavBackStackEntry,androidx.navigation.NavigatorState.OnTransitionCompleteListener>> transitionsInProgress;
- }
-
- public static fun interface NavigatorState.OnTransitionCompleteListener {
- method public void onTransitionComplete();
+ property public final kotlinx.coroutines.flow.StateFlow<java.util.Set<androidx.navigation.NavBackStackEntry>> transitionsInProgress;
}
@androidx.navigation.NavOptionsDsl public final class PopUpToBuilder {
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
index 5a96c53..9991594 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -337,9 +337,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the argument and it should be null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
// Ensure case when matching the exact argument query (i.e. param names in braces) is handled
@@ -374,9 +374,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the argument and it should be null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
@Test
@@ -451,9 +451,9 @@
assertWithMessage("Args should contain the id")
.that(matchArgs?.getInt("id"))
.isEqualTo(id)
- assertWithMessage("Args should contain optional")
- .that(matchArgs?.getString("optional"))
- .isNull()
+ assertWithMessage("Args should not contain optional")
+ .that(matchArgs?.containsKey("optional"))
+ .isFalse()
}
// Make sure we allow extra params that may not been part of the given deep link
@@ -541,9 +541,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the argument and it should be null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
@Test
@@ -579,9 +579,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the argument and it should be null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
@Test
@@ -632,9 +632,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the argument and it should be null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
@Test
@@ -745,12 +745,12 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the first name")
- .that(matchArgs?.getString("first"))
- .isNull()
- assertWithMessage("Args should contain the last name")
- .that(matchArgs?.getString("last"))
- .isNull()
+ assertWithMessage("Args should not contain the first name")
+ .that(matchArgs?.containsKey("first"))
+ .isFalse()
+ assertWithMessage("Args should not contain the last name")
+ .that(matchArgs?.containsKey("last"))
+ .isFalse()
}
@Test
@@ -804,9 +804,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain arg as null")
- .that(matchArgs?.getString("myarg"))
- .isNull()
+ assertWithMessage("Args should not contain the argument")
+ .that(matchArgs?.containsKey("myarg"))
+ .isFalse()
}
// Handle the case were the input is wild card and separator with no argument
@@ -918,9 +918,9 @@
assertWithMessage("Args should not be null")
.that(matchArgs)
.isNotNull()
- assertWithMessage("Args should contain the path as null")
- .that(matchArgs?.getString("path"))
- .isNull()
+ assertWithMessage("Args should not contain the path")
+ .that(matchArgs?.containsKey("path"))
+ .isFalse()
}
// Handle the case were the input could be entire path except for the argument
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
index eef5128..c031139 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorState.kt
@@ -18,7 +18,6 @@
import android.os.Bundle
import androidx.annotation.RestrictTo
-import androidx.navigation.NavigatorState.OnTransitionCompleteListener
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -32,9 +31,8 @@
public abstract class NavigatorState {
private val backStackLock = ReentrantLock(true)
private val _backStack: MutableStateFlow<List<NavBackStackEntry>> = MutableStateFlow(listOf())
- private val _transitionsInProgress:
- MutableStateFlow<Map<NavBackStackEntry, OnTransitionCompleteListener>> =
- MutableStateFlow(mapOf())
+ private val _transitionsInProgress: MutableStateFlow<Set<NavBackStackEntry>> =
+ MutableStateFlow(setOf())
/**
* @hide
@@ -52,13 +50,11 @@
public val backStack: StateFlow<List<NavBackStackEntry>> = _backStack.asStateFlow()
/**
- * This is the map of currently running transitions to their individual
- * [OnTransitionCompleteListener]s. Use this map to retrieve the listener and execute the
- * callback once the transition is complete.
+ * This is the set of currently running transitions. Use this set to retrieve the entry and call
+ * [markTransitionComplete] once the transition is complete.
*/
- public val transitionsInProgress:
- StateFlow<Map<NavBackStackEntry, OnTransitionCompleteListener>> =
- _transitionsInProgress.asStateFlow()
+ public val transitionsInProgress: StateFlow<Set<NavBackStackEntry>> =
+ _transitionsInProgress.asStateFlow()
/**
* Adds the given [backStackEntry] to the [backStack].
@@ -70,15 +66,25 @@
}
/**
- * Provides listener that once activated, adds the given [backStackEntry] to the [backStack].
+ * Adds the given [backStackEntry] to the [backStack]. This also adds the given and
+ * previous entry to the [set of in progress transitions][transitionsInProgress].
+ * Added entries have their [Lifecycle] capped at [Lifecycle.State.STARTED] until an entry is
+ * passed into the [markTransitionComplete] callback, when they are allowed to go to
+ * [Lifecycle.State.RESUMED].
+ *
+ * @see transitionsInProgress
+ * @see markTransitionComplete
+ * @see popWithTransition
*/
- public open fun pushWithTransition(
- backStackEntry: NavBackStackEntry
- ): OnTransitionCompleteListener {
- push(backStackEntry)
- return OnTransitionCompleteListener {
- removeInProgressTransition(backStackEntry)
+ public open fun pushWithTransition(backStackEntry: NavBackStackEntry) {
+ val previousEntry = backStack.value.lastOrNull()
+ // When navigating, we need to mark the outgoing entry as transitioning until it
+ // finishes its outgoing animation.
+ if (previousEntry != null) {
+ _transitionsInProgress.value = _transitionsInProgress.value + previousEntry
}
+ _transitionsInProgress.value = _transitionsInProgress.value + backStackEntry
+ push(backStackEntry)
}
/**
@@ -100,50 +106,46 @@
}
/**
- * Provides listener that once activated, Pops all destinations up to and including [popUpTo].
+ * Pops all destinations up to and including [popUpTo]. This also adds the given and
+ * incoming entry to the [set of in progress transitions][transitionsInProgress]. Added
+ * entries have their [Lifecycle] held at [Lifecycle.State.CREATED] until an entry is
+ * passed into the [markTransitionComplete] callback, when they are allowed to go to
+ * [Lifecycle.State.DESTROYED] and have their state cleared.
*
* This will remove those destinations from the [backStack], saving their state if
* [saveState] is `true`.
+ *
+ * @see transitionsInProgress
+ * @see markTransitionComplete
+ * @see pushWithTransition
*/
- public open fun popWithTransition(
- popUpTo: NavBackStackEntry,
- saveState: Boolean
- ): OnTransitionCompleteListener {
- val listener = OnTransitionCompleteListener {
- removeInProgressTransition(popUpTo)
+ public open fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
+ _transitionsInProgress.value = _transitionsInProgress.value + popUpTo
+ val incomingEntry = backStack.value.lastOrNull { entry ->
+ entry != popUpTo &&
+ backStack.value.lastIndexOf(entry) < backStack.value.lastIndexOf(popUpTo)
+ }
+ // When popping, we need to mark the incoming entry as transitioning so we keep it
+ // STARTED until the transition completes at which point we can move it to RESUMED
+ if (incomingEntry != null) {
+ _transitionsInProgress.value = _transitionsInProgress.value + incomingEntry
}
pop(popUpTo, saveState)
- return listener
}
/**
- * Adds a transition listener to the group of in progress transitions.
+ * This removes the given [NavBackStackEntry] from the [set of the transitions in
+ * progress][transitionsInProgress]. This should be called in conjunction with
+ * [pushWithTransition] and [popWithTransition] as those call are responsible for adding
+ * entries to [transitionsInProgress].
*
- * @hide
+ * Failing to call this method could result in entries being prevented from reaching their
+ * final [Lifecycle.State]}.
+ *
+ * @see pushWithTransition
+ * @see popWithTransition
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun addInProgressTransition(
- entry: NavBackStackEntry,
- listener: OnTransitionCompleteListener
- ) {
- _transitionsInProgress.value = _transitionsInProgress.value + (entry to listener)
- }
-
- /**
- * @hide
- */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun removeInProgressTransition(entry: NavBackStackEntry) {
+ public open fun markTransitionComplete(entry: NavBackStackEntry) {
_transitionsInProgress.value = _transitionsInProgress.value - entry
}
-
- /**
- * OnTransitionCompleteListener receives a callback when a destination transition is complete.
- */
- public fun interface OnTransitionCompleteListener {
- /**
- * Callback for when the transition has completed.
- */
- public fun onTransitionComplete()
- }
}
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 564c176..6448a7e 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -26,15 +26,15 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
- implementation("androidx.compose.foundation:foundation-layout:1.0.0-rc02")
+ implementation("androidx.compose.foundation:foundation-layout:1.0.1")
api("androidx.activity:activity-compose:1.3.0-rc02")
- api("androidx.compose.animation:animation:1.0.0-rc02")
- api(projectOrArtifact(":compose:runtime:runtime"))
- api(projectOrArtifact(":compose:runtime:runtime-saveable"))
- api("androidx.compose.ui:ui:1.0.0-rc02")
+ api("androidx.compose.animation:animation:1.0.1")
+ api("androidx.compose.runtime:runtime:1.0.1")
+ api("androidx.compose.runtime:runtime-saveable:1.0.1")
+ api("androidx.compose.ui:ui:1.0.1")
api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07")
api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index 192e019..088d404 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -25,13 +25,13 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
compileOnly(projectOrArtifact(":annotation:annotation-sampled"))
- implementation("androidx.compose.foundation:foundation:1.0.0-rc02")
+ implementation("androidx.compose.foundation:foundation:1.0.1")
implementation(projectOrArtifact(":navigation:navigation-compose"))
- implementation("androidx.compose.material:material:1.0.0-rc02")
+ implementation("androidx.compose.material:material:1.0.1")
}
androidx {
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
index a35f2d0..84a8da4 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/ComposeNavigator.kt
@@ -60,6 +60,18 @@
}
/**
+ * Callback that removes the given [NavBackStackEntry] from the [map of the transitions in
+ * progress][transitionsInProgress]. This should be called in conjunction with [navigate] and
+ * [popBackStack] as those call are responsible for adding entries to [transitionsInProgress].
+ *
+ * Failing to call this method could result in entries being prevented from reaching their
+ * final [Lifecycle.State]}.
+ */
+ internal fun onTransitionComplete(entry: NavBackStackEntry) {
+ state.markTransitionComplete(entry)
+ }
+
+ /**
* NavDestination specific to [ComposeNavigator]
*/
@NavDestination.ClassType(Composable::class)
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index 62dc370..986422c 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -118,7 +118,7 @@
val backStack by composeNavigator.backStack.collectAsState()
val transitionsInProgress by composeNavigator.transitionsInProgress.collectAsState()
- val backStackEntry = transitionsInProgress.keys.lastOrNull { entry ->
+ val backStackEntry = transitionsInProgress.lastOrNull { entry ->
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
} ?: backStack.lastOrNull { entry ->
entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
@@ -137,13 +137,13 @@
// There's no animation for the initial crossfade,
// so we can instantly mark the transition as complete
transitionsInProgress.forEach { entry ->
- entry.value.onTransitionComplete()
+ composeNavigator.onTransitionComplete(entry)
}
initialCrossfade = false
}
onDispose {
transitionsInProgress.forEach { entry ->
- entry.value.onTransitionComplete()
+ composeNavigator.onTransitionComplete(entry)
}
}
}
diff --git a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
index 9706a9a..10147067 100644
--- a/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
+++ b/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/DialogFragmentNavigatorDestinationBuilder.kt
@@ -122,7 +122,8 @@
* DSL for constructing a new [DialogFragmentNavigator.Destination]
*
* @param navigator navigator used to create the destination
- * @param route the destination's unique route
+ * @param route the destination's unique route. This sets the [route] on the newly
+ * constructed [NavDestination]. This can be any valid non-empty String.
* @param fragmentClass the class name of the DialogFragment to show when you navigate to this
* destination
*/
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
index b061ee6..63c95d2 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryTest.kt
@@ -388,10 +388,55 @@
.isTrue()
}
- private fun createNavController(): NavController {
+ @Suppress("DEPRECATION")
+ @UiThreadTest
+ @Test
+ fun testOnClearedWhenHostClearedAfterSaveStateWithTransitions() {
+ val hostStore = ViewModelStore()
+ val navController = createNavController(true)
+ navController.setViewModelStore(hostStore)
+ val navGraph = navController.navigatorProvider.navigation(
+ id = 1,
+ startDestination = R.id.start_test
+ ) {
+ test(R.id.start_test)
+ }
+ navController.setGraph(navGraph, null)
+
+ val owner = navController.getBackStackEntry(R.id.start_test)
+ assertThat(owner).isNotNull()
+ val viewModel: TestAndroidViewModel = ViewModelProvider(owner).get()
+ assertThat(viewModel.isCleared).isFalse()
+
+ // Navigate to a new instance of start_test, popping the previous one and saving state
+ navController.navigate(
+ R.id.start_test,
+ null,
+ navOptions {
+ popUpTo(R.id.start_test) {
+ inclusive = true
+ saveState = true
+ }
+ }
+ )
+ val newEntry = navController.getBackStackEntry(R.id.start_test)
+ navController.navigatorProvider[TestNavigator::class].onTransitionComplete(newEntry)
+
+ assertWithMessage("ViewModel should be saved when the destination is saved")
+ .that(viewModel.isCleared)
+ .isFalse()
+
+ hostStore.clear()
+
+ assertWithMessage("ViewModel should be cleared when the host is cleared")
+ .that(viewModel.isCleared)
+ .isTrue()
+ }
+
+ private fun createNavController(withTransitions: Boolean = false): NavController {
val navController = NavHostController(ApplicationProvider.getApplicationContext())
navController.setLifecycleOwner(TestLifecycleOwner())
- val navigator = TestNavigator()
+ val navigator = TestNavigator(withTransitions)
navController.navigatorProvider.addNavigator(navigator)
return navController
}
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index de23adb..b80a225 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -181,6 +181,7 @@
mutableMapOf<Navigator<out NavDestination>, NavControllerNavigatorState>()
private var addToBackStackHandler: ((backStackEntry: NavBackStackEntry) -> Unit)? = null
private var popFromBackStackHandler: ((popUpTo: NavBackStackEntry) -> Unit)? = null
+ private val entrySavedState = mutableMapOf<NavBackStackEntry, Boolean>()
/**
* Call [Navigator.navigate] while setting up a [handler] that receives callbacks
@@ -269,43 +270,28 @@
}
}
- override fun pushWithTransition(
- backStackEntry: NavBackStackEntry
- ): OnTransitionCompleteListener {
- val innerListener = super.pushWithTransition(backStackEntry)
- val listener = OnTransitionCompleteListener {
- innerListener.onTransitionComplete()
- if (!this@NavControllerNavigatorState.isNavigating) {
- updateBackStackLifecycle()
- }
- }
- addInProgressTransition(backStackEntry, listener)
- return listener
+ override fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
+ super.popWithTransition(popUpTo, saveState)
+ entrySavedState[popUpTo] = saveState
}
- override fun popWithTransition(
- popUpTo: NavBackStackEntry,
- saveState: Boolean
- ): OnTransitionCompleteListener {
- // we need to mark the entry as transitioning before making the super call to pop so
- // we don't move its lifecycle to DESTROYED.
- addInProgressTransition(popUpTo) { }
- val innerListener = super.popWithTransition(popUpTo, saveState)
- val listener = OnTransitionCompleteListener {
- innerListener.onTransitionComplete()
- if (backQueue.contains(popUpTo)) {
- updateBackStackLifecycle()
- } else {
- // If the entry is no longer part of the backStack, we need to manually move
- // it to DESTROYED, and clear its view model
- popUpTo.maxLifecycle = Lifecycle.State.DESTROYED
- if (!saveState) {
- viewModel?.clear(popUpTo.id)
- }
+ override fun markTransitionComplete(entry: NavBackStackEntry) {
+ val savedState = entrySavedState[entry] == true
+ super.markTransitionComplete(entry)
+ entrySavedState.remove(entry)
+ if (!backQueue.contains(entry)) {
+ // If the entry is no longer part of the backStack, we need to manually move
+ // it to DESTROYED, and clear its view model
+ entry.maxLifecycle = Lifecycle.State.DESTROYED
+ if (!savedState) {
+ viewModel?.clear(entry.id)
}
+ updateBackStackLifecycle()
+ } else if (!this@NavControllerNavigatorState.isNavigating) {
+ updateBackStackLifecycle()
}
- addInProgressTransition(popUpTo, listener)
- return listener
+ // else, updateBackStackLifecycle() will be called after any ongoing navigate() call
+ // completes
}
}
@@ -604,17 +590,7 @@
val navigator = navigatorProvider
.getNavigator<Navigator<NavDestination>>(entry.destination.navigatorName)
val state = navigatorState[navigator]
- val transitioning = state?.transitionsInProgress?.value?.containsKey(entry)
- // When popping, we need to mark the incoming entry as transitioning so we keep it
- // STARTED until the transition completes at which point we can move it to RESUMED
- if (backQueue.isNotEmpty() && transitioning == true) {
- state.addInProgressTransition(backQueue.last()) {
- state.removeInProgressTransition(backQueue.last())
- if (!state.isNavigating) {
- updateBackStackLifecycle()
- }
- }
- }
+ val transitioning = state?.transitionsInProgress?.value?.contains(entry)
if (entry.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
if (saveState) {
// Move the state through STOPPED
@@ -862,7 +838,7 @@
val navigator = navigatorProvider
.getNavigator<Navigator<*>>(entry.destination.navigatorName)
val state = navigatorState[navigator]
- val transitioning = state?.transitionsInProgress?.value?.containsKey(entry)
+ val transitioning = state?.transitionsInProgress?.value?.contains(entry)
if (transitioning != true) {
upwardStateTransitions[entry] = Lifecycle.State.RESUMED
} else {
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index 0685776..7be84d4 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -103,7 +103,7 @@
assertThat(firstEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
- state.transitionsInProgress.value[firstEntry]?.onTransitionComplete()
+ state.markTransitionComplete(firstEntry)
assertThat(firstEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
@@ -113,23 +113,38 @@
.isEqualTo(Lifecycle.State.CREATED)
assertThat(secondEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
- state.transitionsInProgress.value[secondEntry]?.onTransitionComplete()
+ state.markTransitionComplete(firstEntry)
+ state.markTransitionComplete(secondEntry)
assertThat(secondEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- navigator.popBackStack(secondEntry, false)
+ navigator.popBackStack(secondEntry, true)
assertThat(secondEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.CREATED)
assertThat(firstEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.STARTED)
- state.transitionsInProgress.value[firstEntry]?.onTransitionComplete()
+ state.markTransitionComplete(firstEntry)
assertThat(firstEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.RESUMED)
- state.transitionsInProgress.value[secondEntry]?.onTransitionComplete()
+ state.markTransitionComplete(secondEntry)
assertThat(secondEntry.lifecycle.currentState)
.isEqualTo(Lifecycle.State.DESTROYED)
+
+ val restoredSecondEntry = state.restoreBackStackEntry(secondEntry)
+ navigator.navigate(listOf(restoredSecondEntry), null, null)
+ assertThat(firstEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.CREATED)
+ assertThat(restoredSecondEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.STARTED)
+ assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+ state.markTransitionComplete(firstEntry)
+ state.markTransitionComplete(restoredSecondEntry)
+ assertThat(restoredSecondEntry.lifecycle.currentState)
+ .isEqualTo(Lifecycle.State.RESUMED)
}
@Navigator.Name("test")
diff --git a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
index eef9c77..c146ce31 100644
--- a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
+++ b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
@@ -27,7 +27,6 @@
import androidx.navigation.NavDestination
import androidx.navigation.NavViewModelStoreProvider
import androidx.navigation.NavigatorState
-import androidx.navigation.NavigatorState.OnTransitionCompleteListener
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
@@ -66,6 +65,7 @@
}
private val savedStates = mutableMapOf<String, Bundle>()
+ private val entrySavedState = mutableMapOf<NavBackStackEntry, Boolean>()
override fun createBackStackEntry(
destination: NavDestination,
@@ -96,19 +96,6 @@
updateMaxLifecycle()
}
- override fun pushWithTransition(
- backStackEntry: NavBackStackEntry
- ): OnTransitionCompleteListener {
- val innerListener = super.pushWithTransition(backStackEntry)
- val listener = OnTransitionCompleteListener {
- innerListener.onTransitionComplete()
- updateMaxLifecycle()
- }
- addInProgressTransition(backStackEntry, listener)
- updateMaxLifecycle()
- return listener
- }
-
override fun pop(popUpTo: NavBackStackEntry, saveState: Boolean) {
val beforePopList = backStack.value
val poppedList = beforePopList.subList(beforePopList.indexOf(popUpTo), beforePopList.size)
@@ -116,35 +103,20 @@
updateMaxLifecycle(poppedList, saveState)
}
- override fun popWithTransition(
- popUpTo: NavBackStackEntry,
- saveState: Boolean
- ): OnTransitionCompleteListener {
- // Get the entry that will be incoming after we have popped all the way up to the desired
- // entry.
- // We need to do this before we call popWithTransition because for the TestNavigatorState
- // pop is called immediately, which would cause the entry to immediately go to RESUMED.
- val incomingEntry = backStack.value.lastOrNull { entry ->
- entry != popUpTo &&
- backStack.value.lastIndexOf(entry) < backStack.value.lastIndexOf(popUpTo)
+ override fun popWithTransition(popUpTo: NavBackStackEntry, saveState: Boolean) {
+ super.popWithTransition(popUpTo, saveState)
+ entrySavedState[popUpTo] = saveState
+ }
+
+ override fun markTransitionComplete(entry: NavBackStackEntry) {
+ val savedState = entrySavedState[entry] == true
+ super.markTransitionComplete(entry)
+ entrySavedState.remove(entry)
+ if (!backStack.value.contains(entry)) {
+ updateMaxLifecycle(listOf(entry), savedState)
+ } else {
+ updateMaxLifecycle()
}
- // When popping, we need to mark the incoming entry as transitioning so we keep it
- // STARTED until the transition completes at which point we can move it to RESUMED
- if (incomingEntry != null) {
- addInProgressTransition(incomingEntry) {
- removeInProgressTransition(incomingEntry)
- updateMaxLifecycle()
- }
- }
- addInProgressTransition(popUpTo) { }
- val innerListener = super.popWithTransition(popUpTo, saveState)
- val listener = OnTransitionCompleteListener {
- innerListener.onTransitionComplete()
- updateMaxLifecycle(listOf(popUpTo))
- }
- addInProgressTransition(popUpTo, listener)
- updateMaxLifecycle()
- return listener
}
private fun updateMaxLifecycle(
@@ -158,16 +130,22 @@
withContext(Dispatchers.Main.immediate) {
// Mark all removed NavBackStackEntries as DESTROYED
for (entry in poppedList.reversed()) {
- if (saveState) {
+ if (
+ saveState &&
+ entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
+ ) {
// Move the NavBackStackEntry to the stopped state, then save its state
entry.maxLifecycle = Lifecycle.State.CREATED
val savedState = Bundle()
entry.saveState(savedState)
savedStates[entry.id] = savedState
}
- val transitioning = transitionsInProgress.value.containsKey(entry)
+ val transitioning = transitionsInProgress.value.contains(entry)
if (!transitioning) {
entry.maxLifecycle = Lifecycle.State.DESTROYED
+ if (!saveState) {
+ savedStates.remove(entry.id)
+ }
} else {
entry.maxLifecycle = Lifecycle.State.CREATED
}
@@ -176,7 +154,7 @@
val currentList = backStack.value
var previousEntry: NavBackStackEntry? = null
for (entry in currentList.reversed()) {
- val transitioning = transitionsInProgress.value.containsKey(entry)
+ val transitioning = transitionsInProgress.value.contains(entry)
entry.maxLifecycle = when {
previousEntry == null ->
if (!transitioning) {
diff --git a/paging/paging-compose/build.gradle b/paging/paging-compose/build.gradle
index f6892b8..de8ffd0 100644
--- a/paging/paging-compose/build.gradle
+++ b/paging/paging-compose/build.gradle
@@ -28,10 +28,10 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
- api(projectOrArtifact(":compose:foundation:foundation"))
+ api("androidx.compose.foundation:foundation:1.0.1")
api("androidx.paging:paging-common:3.0.0")
androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
diff --git a/paging/paging-compose/samples/build.gradle b/paging/paging-compose/samples/build.gradle
index 8bd229e..5b7a3c1 100644
--- a/paging/paging-compose/samples/build.gradle
+++ b/paging/paging-compose/samples/build.gradle
@@ -26,12 +26,12 @@
}
dependencies {
- kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
+ kotlinPlugin("androidx.compose.compiler:compiler:1.0.1")
implementation(libs.kotlinStdlib)
compileOnly(projectOrArtifact(":annotation:annotation-sampled"))
- implementation("androidx.compose.foundation:foundation:1.0.0-rc02")
- implementation("androidx.compose.material:material:1.0.0-rc02")
+ implementation("androidx.compose.foundation:foundation:1.0.1")
+ implementation("androidx.compose.material:material:1.0.1")
implementation(projectOrArtifact(":paging:paging-compose"))
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
index aec2c6e..8e0b73e 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/PagingSourceTest.kt
@@ -30,11 +30,14 @@
import androidx.room.Insert
import androidx.room.PrimaryKey
import androidx.room.Query
+import androidx.room.RawQuery
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.awaitPendingRefresh
import androidx.room.pendingRefresh
import androidx.room.refreshRunnable
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.sqlite.db.SupportSQLiteQuery
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.base.MainThread
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -162,6 +165,43 @@
}
@Test
+ fun loadEverythingRawQuery() {
+ // open db
+ val items = createItems(startId = 15, count = 50)
+ db.dao.insert(items)
+ val query = SimpleSQLiteQuery(
+ "SELECT * FROM PagingEntity ORDER BY id ASC"
+ )
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = { db.dao.loadItemsRaw(query) }
+ )
+ runTest(pager = pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ items.createExpected(
+ fromIndex = 0,
+ toIndex = CONFIG.initialLoadSize
+ )
+ )
+ // now access more items that should trigger loading more
+ withContext(Dispatchers.Main) {
+ itemStore.get(20)
+ }
+ assertThat(itemStore.awaitItem(20)).isEqualTo(items[20])
+ // now access to the end of the list, it should load everything as we disabled jumping
+ withContext(Dispatchers.Main) {
+ itemStore.get(items.size - 1)
+ }
+ assertThat(itemStore.awaitItem(items.size - 1)).isEqualTo(items.last())
+ assertThat(itemStore.peekItems()).isEqualTo(items)
+ assertThat(itemStore.currentGenerationId).isEqualTo(1)
+ }
+ }
+
+ @Test
fun loadEverything_inReverse() {
// open db
val items = createItems(startId = 0, count = 100)
@@ -200,6 +240,210 @@
}
@Test
+ fun loadEverythingRawQuery_inReverse() {
+ // open db
+ val items = createItems(startId = 0, count = 100)
+ db.dao.insert(items)
+ val query = SimpleSQLiteQuery(
+ "SELECT * FROM PagingEntity ORDER BY id ASC"
+ )
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 98,
+ pagingSourceFactory = { db.dao.loadItemsRaw(query) }
+ )
+ runTest(pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ items.createExpected(
+ // Paging 3 implementation loads starting from initial key
+ fromIndex = 98,
+ toIndex = 100
+ )
+ )
+ // now access more items that should trigger loading more
+ withContext(Dispatchers.Main) {
+ itemStore.get(40)
+ }
+ assertThat(itemStore.awaitItem(40)).isEqualTo(items[40])
+ // now access to the beginning of the list, it should load everything as we don't
+ // support jumping
+ withContext(Dispatchers.Main) {
+ itemStore.get(0)
+ }
+ assertThat(itemStore.awaitItem(0)).isEqualTo(items[0])
+ assertThat(itemStore.peekItems()).isEqualTo(items)
+ assertThat(itemStore.currentGenerationId).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun rawQuery_userSuppliedLimitOffset() {
+ val items = createItems(startId = 15, count = 70)
+ db.dao.insert(items)
+
+ val query = SimpleSQLiteQuery(
+ "SELECT * FROM PagingEntity ORDER BY id ASC LIMIT 30 OFFSET 5"
+ )
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = { db.dao.loadItemsRaw(query) }
+ )
+
+ runTest(pager = pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ // returns items 20 to 28 with 21 null place holders after
+ items.createBoundedExpected(
+ fromIndex = 5,
+ toIndex = 5 + CONFIG.initialLoadSize,
+ toPlaceholderIndex = 35,
+ )
+ )
+ // now access more items that should trigger loading more
+ withContext(Dispatchers.Main) {
+ itemStore.get(15)
+ }
+ // item 15 is offset by 5 = 20
+ assertThat(itemStore.awaitItem(15)).isEqualTo(items[20])
+ // normally itemStore.get(50) is valid, but user-set LIMIT should bound item count to 30
+ // itemStore.get(50) should now become invalid
+ val expectedException = assertFailsWith<IndexOutOfBoundsException> {
+ withContext(Dispatchers.Main) {
+ itemStore.get(50)
+ }
+ }
+ assertThat(expectedException.message).isEqualTo("Index: 50, Size: 30")
+ assertThat(itemStore.currentGenerationId).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun rawQuery_multipleArguments() {
+ val items = createItems(startId = 0, count = 80)
+ db.dao.insert(items)
+ val query = SimpleSQLiteQuery(
+ "SELECT * " +
+ "FROM PagingEntity " +
+ "WHERE id > 49 AND id < 76 " +
+ "ORDER BY id ASC " +
+ "LIMIT 20"
+ )
+ val pager = Pager(
+ config = CONFIG,
+ pagingSourceFactory = { db.dao.loadItemsRaw(query) }
+ )
+
+ runTest(pager = pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ // returns items 50 to 58 with 11 null place holders after
+ items.createBoundedExpected(
+ fromIndex = 50,
+ toIndex = 50 + CONFIG.initialLoadSize,
+ toPlaceholderIndex = 70,
+ )
+ )
+ // now access more items that should trigger loading more
+ withContext(Dispatchers.Main) {
+ itemStore.get(15)
+ }
+ // item 15 is offset by 50 because of `WHERE id > 49` arg
+ assertThat(itemStore.awaitItem(15)).isEqualTo(items[65])
+ // normally itemStore.get(50) is valid, but user-set LIMIT should bound item count to 20
+ val expectedException = assertFailsWith<IndexOutOfBoundsException> {
+ withContext(Dispatchers.Main) {
+ itemStore.get(50)
+ }
+ }
+ assertThat(expectedException.message).isEqualTo("Index: 50, Size: 20")
+ assertThat(itemStore.currentGenerationId).isEqualTo(1)
+ }
+ }
+
+ @Test
+ fun keyTooLarge_returnLastPage() {
+ val items = createItems(startId = 0, count = 50)
+ db.dao.insert(items)
+
+ val pager = Pager(
+ config = CONFIG,
+ initialKey = 80
+ ) {
+ db.dao.loadItems()
+ }
+ runTest(pager = pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ // should return last page when key is too large
+ items.createExpected(
+ fromIndex = 41,
+ toIndex = 50,
+ )
+ )
+ // now trigger a prepend
+ withContext(Dispatchers.Main) {
+ itemStore.get(20)
+ }
+ assertThat(itemStore.awaitItem(20)).isEqualTo(items[20])
+ }
+ }
+
+ @Test
+ fun jumping() {
+ val items = createItems(startId = 0, count = 200)
+ db.dao.insert(items)
+
+ val config = PagingConfig(
+ pageSize = 3,
+ initialLoadSize = 9,
+ enablePlaceholders = true,
+ jumpThreshold = 80
+ )
+ val pager = Pager(
+ config = config,
+ ) {
+ db.dao.loadItems()
+ }
+ runTest(pager = pager) {
+ itemStore.awaitInitialLoad()
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ items.createExpected(
+ fromIndex = 0,
+ toIndex = config.initialLoadSize,
+ )
+ )
+ // now trigger a jump, accessed index needs to be larger than jumpThreshold
+ withContext(Dispatchers.Main) {
+ itemStore.get(120)
+ }
+ // the jump should trigger a refresh load with new generation
+ itemStore.awaitGeneration(2)
+ itemStore.awaitInitialLoad()
+ // the refresh should load around anchorPosition of 120, with refresh key as 116
+ // and null placeholders before and after
+ assertThat(
+ itemStore.peekItems()
+ ).containsExactlyElementsIn(
+ items.createExpected(
+ fromIndex = 116,
+ toIndex = 116 + config.initialLoadSize,
+ )
+ )
+ }
+ }
+
+ @Test
fun prependWithDelayedInvalidation() {
val items = createItems(startId = 0, count = 90)
db.dao.insert(items)
@@ -441,7 +685,7 @@
*/
private fun List<PagingEntity?>.createExpected(
fromIndex: Int,
- toIndex: Int
+ toIndex: Int,
): List<PagingEntity?> {
val result = mutableListOf<PagingEntity?>()
(0 until fromIndex).forEach { _ -> result.add(null) }
@@ -450,6 +694,17 @@
return result
}
+ private fun List<PagingEntity?>.createBoundedExpected(
+ fromIndex: Int,
+ toIndex: Int,
+ toPlaceholderIndex: Int,
+ ): List<PagingEntity?> {
+ val result = mutableListOf<PagingEntity?>()
+ result.addAll(this.subList(fromIndex, toIndex))
+ (toIndex until toPlaceholderIndex).forEach { _ -> result.add(null) }
+ return result
+ }
+
@Database(
version = 1,
exportSchema = false,
@@ -497,6 +752,9 @@
@Query("SELECT * FROM PagingEntity ORDER BY id ASC")
fun loadItems(): PagingSource<Int, PagingEntity>
+
+ @RawQuery(observedEntities = [PagingEntity::class])
+ fun loadItemsRaw(query: SupportSQLiteQuery): PagingSource<Int, PagingEntity>
}
/**
@@ -683,7 +941,7 @@
private val CONFIG = PagingConfig(
pageSize = 3,
initialLoadSize = 9,
- enablePlaceholders = true
+ enablePlaceholders = true,
)
}
}
diff --git a/settings.gradle b/settings.gradle
index 59dc14e..d312fea 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -631,6 +631,7 @@
includeProject(":wear:compose:compose-foundation-samples", "wear/compose/compose-foundation/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material", "wear/compose/compose-material", [BuildType.COMPOSE])
includeProject(":wear:compose:compose-material-benchmark", "wear/compose/compose-material/benchmark", [BuildType.COMPOSE])
+includeProject(":wear:compose:compose-material-samples", "wear/compose/compose-material/samples", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:demos", "wear/compose/integration-tests/demos", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:macrobenchmark", "wear/compose/integration-tests/macrobenchmark", [BuildType.COMPOSE])
includeProject(":wear:compose:integration-tests:macrobenchmark-target", "wear/compose/integration-tests/macrobenchmark-target", [BuildType.COMPOSE])
diff --git a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigator.kt b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigator.kt
index f4b4933..6adce96 100644
--- a/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigator.kt
+++ b/testutils/testutils-navigation/src/main/java/androidx/testutils/TestNavigator.kt
@@ -18,13 +18,15 @@
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination
+import androidx.navigation.NavOptions
import androidx.navigation.Navigator
/**
* A simple Navigator that doesn't actually navigate anywhere, but does dispatch correctly
*/
@Navigator.Name("test")
-open class TestNavigator : Navigator<TestNavigator.Destination>() {
+open class TestNavigator(private val hasTransitions: Boolean = false) :
+ Navigator<TestNavigator.Destination>() {
val backStack: List<NavBackStackEntry>
get() = state.backStack.value
@@ -41,6 +43,32 @@
return Destination(this)
}
+ override fun navigate(
+ entries: List<NavBackStackEntry>,
+ navOptions: NavOptions?,
+ navigatorExtras: Extras?
+ ) {
+ entries.forEach { entry ->
+ if (hasTransitions) {
+ state.pushWithTransition(entry)
+ } else {
+ state.push(entry)
+ }
+ }
+ }
+
+ override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+ if (hasTransitions) {
+ state.popWithTransition(popUpTo, savedState)
+ } else {
+ super.popBackStack(popUpTo, savedState)
+ }
+ }
+
+ public fun onTransitionComplete(entry: NavBackStackEntry) {
+ state.markTransitionComplete(entry)
+ }
+
/**
* A simple Test destination
*/
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 5f663c0..ec09db5 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -33,7 +33,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
@@ -138,6 +138,10 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
+ public final class ListHeaderKt {
+ method @androidx.compose.runtime.Composable public static void ListHeader(optional androidx.compose.ui.Modifier modifier, optional long contentColor, optional long backgroundColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class MaterialTextSelectionColorsKt {
}
@@ -155,6 +159,55 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.wear.compose.material.Colors colors, optional androidx.wear.compose.material.Typography typography, optional androidx.wear.compose.material.Shapes shapes, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class ScalingLazyColumnDefaults {
+ method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+ field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+ }
+
+ public final class ScalingLazyColumnKt {
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyColumnState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyColumnScope,kotlin.Unit> content);
+ }
+
+ public final class ScalingLazyColumnMeasureKt {
+ }
+
+ public interface ScalingLazyColumnScope {
+ method public void item(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public void items(int count, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @androidx.compose.runtime.Stable public final class ScalingLazyColumnState {
+ ctor public ScalingLazyColumnState();
+ field public static final androidx.wear.compose.material.ScalingLazyColumnState.Companion Companion;
+ }
+
+ public static final class ScalingLazyColumnState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> Saver;
+ }
+
+ public final class ScalingLazyColumnStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyColumnState rememberScalingLazyColumnState();
+ }
+
+ public interface ScalingParams {
+ method public float getEdgeAlpha();
+ method public float getEdgeScale();
+ method public float getMaxElementHeight();
+ method public float getMaxTransitionArea();
+ method public float getMinElementHeight();
+ method public float getMinTransitionArea();
+ method public androidx.compose.animation.core.Easing getScaleInterpolator();
+ method public int resolveViewportVerticalOffset(long viewportConstraints);
+ property public abstract float edgeAlpha;
+ property public abstract float edgeScale;
+ property public abstract float maxElementHeight;
+ property public abstract float maxTransitionArea;
+ property public abstract float minElementHeight;
+ property public abstract float minTransitionArea;
+ property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ }
+
@androidx.compose.runtime.Immutable public final class Shapes {
ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 5f663c0..ec09db5 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -33,7 +33,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
@@ -138,6 +138,10 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
+ public final class ListHeaderKt {
+ method @androidx.compose.runtime.Composable public static void ListHeader(optional androidx.compose.ui.Modifier modifier, optional long contentColor, optional long backgroundColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class MaterialTextSelectionColorsKt {
}
@@ -155,6 +159,55 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.wear.compose.material.Colors colors, optional androidx.wear.compose.material.Typography typography, optional androidx.wear.compose.material.Shapes shapes, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class ScalingLazyColumnDefaults {
+ method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+ field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+ }
+
+ public final class ScalingLazyColumnKt {
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyColumnState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyColumnScope,kotlin.Unit> content);
+ }
+
+ public final class ScalingLazyColumnMeasureKt {
+ }
+
+ public interface ScalingLazyColumnScope {
+ method public void item(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public void items(int count, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @androidx.compose.runtime.Stable public final class ScalingLazyColumnState {
+ ctor public ScalingLazyColumnState();
+ field public static final androidx.wear.compose.material.ScalingLazyColumnState.Companion Companion;
+ }
+
+ public static final class ScalingLazyColumnState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> Saver;
+ }
+
+ public final class ScalingLazyColumnStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyColumnState rememberScalingLazyColumnState();
+ }
+
+ public interface ScalingParams {
+ method public float getEdgeAlpha();
+ method public float getEdgeScale();
+ method public float getMaxElementHeight();
+ method public float getMaxTransitionArea();
+ method public float getMinElementHeight();
+ method public float getMinTransitionArea();
+ method public androidx.compose.animation.core.Easing getScaleInterpolator();
+ method public int resolveViewportVerticalOffset(long viewportConstraints);
+ property public abstract float edgeAlpha;
+ property public abstract float edgeScale;
+ property public abstract float maxElementHeight;
+ property public abstract float maxTransitionArea;
+ property public abstract float minElementHeight;
+ property public abstract float minTransitionArea;
+ property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ }
+
@androidx.compose.runtime.Immutable public final class Shapes {
ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 5f663c0..ec09db5 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -33,7 +33,7 @@
method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter cardBackgroundPainter(optional long startBackgroundColor, optional long endBackgroundColor, optional androidx.compose.ui.unit.LayoutDirection gradientDirection);
method public float getAppImageSize();
method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
- method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
+ method @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.painter.Painter imageWithScrimBackgroundPainter(androidx.compose.ui.graphics.painter.Painter backgroundImagePainter, optional androidx.compose.ui.graphics.Brush backgroundImageScrimBrush);
property public final float AppImageSize;
property public final androidx.compose.foundation.layout.PaddingValues ContentPadding;
field public static final androidx.wear.compose.material.CardDefaults INSTANCE;
@@ -138,6 +138,10 @@
method @androidx.compose.runtime.Composable public static void Icon(androidx.compose.ui.graphics.painter.Painter painter, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional long tint);
}
+ public final class ListHeaderKt {
+ method @androidx.compose.runtime.Composable public static void ListHeader(optional androidx.compose.ui.Modifier modifier, optional long contentColor, optional long backgroundColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ }
+
public final class MaterialTextSelectionColorsKt {
}
@@ -155,6 +159,55 @@
method @androidx.compose.runtime.Composable public static void MaterialTheme(optional androidx.wear.compose.material.Colors colors, optional androidx.wear.compose.material.Typography typography, optional androidx.wear.compose.material.Shapes shapes, kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class ScalingLazyColumnDefaults {
+ method public androidx.wear.compose.material.ScalingParams scalingParams(optional float edgeScale, optional float edgeAlpha, optional float minElementHeight, optional float maxElementHeight, optional float minTransitionArea, optional float maxTransitionArea, optional androidx.compose.animation.core.Easing scaleInterpolator, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.unit.Constraints,java.lang.Integer> viewportVerticalOffsetResolver);
+ field public static final androidx.wear.compose.material.ScalingLazyColumnDefaults INSTANCE;
+ }
+
+ public final class ScalingLazyColumnKt {
+ method @androidx.compose.runtime.Composable public static void ScalingLazyColumn(optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ScalingParams scalingParams, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional androidx.compose.ui.Alignment.Horizontal horizontalAlignment, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.wear.compose.material.ScalingLazyColumnState state, kotlin.jvm.functions.Function1<? super androidx.wear.compose.material.ScalingLazyColumnScope,kotlin.Unit> content);
+ }
+
+ public final class ScalingLazyColumnMeasureKt {
+ }
+
+ public interface ScalingLazyColumnScope {
+ method public void item(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+ method public void items(int count, kotlin.jvm.functions.Function1<? super java.lang.Integer,kotlin.Unit> itemContent);
+ }
+
+ @androidx.compose.runtime.Stable public final class ScalingLazyColumnState {
+ ctor public ScalingLazyColumnState();
+ field public static final androidx.wear.compose.material.ScalingLazyColumnState.Companion Companion;
+ }
+
+ public static final class ScalingLazyColumnState.Companion {
+ method public androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> getSaver();
+ property public final androidx.compose.runtime.saveable.Saver<androidx.wear.compose.material.ScalingLazyColumnState,?> Saver;
+ }
+
+ public final class ScalingLazyColumnStateKt {
+ method @androidx.compose.runtime.Composable public static androidx.wear.compose.material.ScalingLazyColumnState rememberScalingLazyColumnState();
+ }
+
+ public interface ScalingParams {
+ method public float getEdgeAlpha();
+ method public float getEdgeScale();
+ method public float getMaxElementHeight();
+ method public float getMaxTransitionArea();
+ method public float getMinElementHeight();
+ method public float getMinTransitionArea();
+ method public androidx.compose.animation.core.Easing getScaleInterpolator();
+ method public int resolveViewportVerticalOffset(long viewportConstraints);
+ property public abstract float edgeAlpha;
+ property public abstract float edgeScale;
+ property public abstract float maxElementHeight;
+ property public abstract float maxTransitionArea;
+ property public abstract float minElementHeight;
+ property public abstract float minTransitionArea;
+ property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
+ }
+
@androidx.compose.runtime.Immutable public final class Shapes {
ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/samples/build.gradle b/wear/compose/compose-material/samples/build.gradle
new file mode 100644
index 0000000..748fd30
--- /dev/null
+++ b/wear/compose/compose-material/samples/build.gradle
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+import androidx.build.LibraryVersions
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("AndroidXComposePlugin")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ kotlinPlugin(project(":compose:compiler:compiler"))
+
+ implementation(libs.kotlinStdlib)
+
+ compileOnly(project(":annotation:annotation-sampled"))
+ implementation(project(":compose:foundation:foundation"))
+ implementation(project(":compose:foundation:foundation-layout"))
+ implementation(project(":compose:runtime:runtime"))
+ implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-text"))
+ implementation(project(":wear:compose:compose-material"))
+}
+
+androidx {
+ name = "Android Wear Compose Material Samples"
+ type = LibraryType.SAMPLES
+ mavenGroup = LibraryGroups.WEAR_COMPOSE
+ inceptionYear = "2021"
+ description = "Contains the sample code for the Android Wear Compose Material Classes"
+}
diff --git a/wear/compose/compose-material/samples/src/main/AndroidManifest.xml b/wear/compose/compose-material/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..67639fa
--- /dev/null
+++ b/wear/compose/compose-material/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="androidx.wear.compose.material.samples">
+ <uses-sdk android:minSdkVersion="25"
+ tools:overrideLibrary="androidx.wear.compose.material"/>
+</manifest>
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
new file mode 100644
index 0000000..53aea17
--- /dev/null
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/ScalingLazyColumnSample.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.Text
+
+@Sampled
+@Composable
+fun SimpleScalingLazyColumn() {
+ ScalingLazyColumn() {
+ items(20) {
+ Chip(
+ onClick = { },
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors()
+ )
+ }
+ }
+}
+
+@Composable
+fun ScalingLazyColumnWithHeaders() {
+ ScalingLazyColumn {
+ item { Spacer(modifier = Modifier.size(20.dp)) }
+ item { ListHeader { Text("Header1") } }
+ items(5) {
+ Chip(
+ onClick = { },
+ label = { Text("List item $it") },
+ colors = ChipDefaults.secondaryChipColors()
+ )
+ }
+ item { ListHeader { Text("Header2") } }
+ items(5) {
+ Chip(
+ onClick = { },
+ label = { Text("List item ${it + 5}") },
+ colors = ChipDefaults.secondaryChipColors()
+ )
+ }
+ }
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
index 798ec64..aaba692f 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Card.kt
@@ -380,7 +380,7 @@
* image to ensure that any text drawn over the image is legible
*/
@Composable
- public fun imageBackgroundPainter(
+ public fun imageWithScrimBackgroundPainter(
backgroundImagePainter: Painter,
backgroundImageScrimBrush: Brush = Brush.linearGradient(
colors = listOf(
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ListHeader.kt
new file mode 100644
index 0000000..df7fe38
--- /dev/null
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ListHeader.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+
+/**
+ * A slot based composable for creating a list header item. List header items are typically expected
+ * to be text. The contents provided will have text and colors effects applied based on the
+ * MaterialTheme. The contents will be start and end padded by default and will fill the max width
+ * of the parent.
+ *
+ * Example usage:
+ * @sample androidx.wear.compose.material.samples.ScalingLazyColumnWithHeaders
+
+ * @param modifier The modifier for the list header
+ * @param contentColor The color to apply to content
+ * @param backgroundColor The background color to apply - typically Color.Tranparent
+ */
+@Composable
+public fun ListHeader(
+ modifier: Modifier = Modifier,
+ contentColor: Color = MaterialTheme.colors.onSurfaceVariant2,
+ backgroundColor: Color = Color.Transparent,
+ content: @Composable () -> Unit
+) {
+ Box(
+ modifier = modifier.height(48.dp)
+ .fillMaxWidth()
+ .wrapContentSize()
+ .background(backgroundColor)
+ .padding(horizontal = 14.dp)
+ ) {
+ CompositionLocalProvider(
+ LocalContentColor provides contentColor,
+ LocalTextStyle provides MaterialTheme.typography.button,
+ content = content
+ )
+ }
+}
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
new file mode 100644
index 0000000..64bfa92
--- /dev/null
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumn.kt
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material
+
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * Receiver scope which is used by [ScalingLazyColumn].
+ */
+public interface ScalingLazyColumnScope {
+ /**
+ * Adds a single item.
+ *
+ * @param content the content of the item
+ */
+ fun item(content: @Composable () -> Unit)
+
+ /**
+ * Adds a [count] of items.
+ *
+ * @param count the items count
+ * @param itemContent the content displayed by a single item
+ */
+ fun items(count: Int, itemContent: @Composable (Int) -> Unit)
+}
+
+/**
+ * A scrolling scaling/fisheye list component that forms a key part of the Wear Material Design
+ * language. Provides scaling and transparency effects to the content items.
+ *
+ * [ScalingLazyColumn] is designed to be able to handle potentially large numbers of content
+ * items. Content items are only materialized and composed when needed.
+ *
+ * If scaling/fisheye functionality is not required then a [LazyColumn] should be considered
+ * instead to avoid any overhead of measuring and calculating scaling and transparency effects for
+ * the content items.
+ *
+ * Example usage:
+ * @sample androidx.wear.compose.material.samples.SimpleScalingLazyColumn
+ *
+ * @param modifier The modifier to be applied to the component
+ * @param scalingParams The parameters to configure the scaling and transparency effects for the
+ * component
+ * @param verticalArrangement The vertical arrangement of the layout's children. This allows us
+ * to add spacing between items and specify the arrangement of the items when we have not enough
+ * of them to fill the whole minimum size
+ * @param horizontalAlignment the horizontal alignment applied to the items
+ * @param contentPadding The padding to apply around the contents
+ * @param state The state of the component
+ */
+@Composable
+public fun ScalingLazyColumn(
+ modifier: Modifier = Modifier,
+ scalingParams: ScalingParams = ScalingLazyColumnDefaults.scalingParams(),
+ verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp),
+ horizontalAlignment: Alignment.Horizontal = Alignment.Start,
+ contentPadding: PaddingValues = PaddingValues(horizontal = 8.dp),
+ state: ScalingLazyColumnState = rememberScalingLazyColumnState(),
+ content: ScalingLazyColumnScope.() -> Unit
+) {
+ BoxWithConstraints(modifier = modifier) {
+
+ val extraPaddingInPixels = scalingParams.resolveViewportVerticalOffset(constraints)
+ val extraPadding = with(LocalDensity.current) { extraPaddingInPixels.toDp() }
+ val combinedPaddingValues = CombinedPaddingValues(
+ contentPadding = contentPadding,
+ extraPadding = extraPadding
+ )
+ LazyColumn(
+ // TODO (b/194464849): Refactor by adding a Modifier.verticalNegativePadding fun
+ Modifier
+ .fillMaxSize()
+ .clipToBounds()
+ .layout { measurable, constraints ->
+ require(constraints.hasBoundedWidth)
+ require(constraints.hasBoundedHeight)
+ val placeable = measurable.measure(
+ Constraints.fixed(
+ width = constraints.maxWidth,
+ height = constraints.maxHeight +
+ (extraPadding * 2).roundToPx()
+ )
+ )
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ placeable.place(0, -extraPadding.roundToPx())
+ }
+ },
+ horizontalAlignment = horizontalAlignment,
+ contentPadding = combinedPaddingValues,
+ verticalArrangement = verticalArrangement,
+ state = state.lazyListState
+ ) {
+ val scope = ScalingLazyColumnScopeImpl(
+ state.lazyListState,
+ this,
+ scalingParams,
+ maxHeight,
+ verticalArrangement.spacing
+ )
+ scope.content()
+ }
+ }
+}
+
+/**
+ * Contains the default values used by [ScalingLazyColumn]
+ */
+public object ScalingLazyColumnDefaults {
+ /**
+ * Creates a [ScalingParams] that represents the scaling and alpha properties for a
+ * [ScalingLazyColumn].
+ *
+ * @param edgeScale What fraction of the full size of the item to scale it by when most
+ * scaled, e.g. at the [minTransitionArea] line. A value between [0.0,1.0], so a value of 0.2f
+ * means to scale an item to 20% of its normal size.
+ *
+ * @param edgeAlpha What fraction of the full transparency of the item to scale it by when
+ * most scaled, e.g. at the [minTransitionArea] line. A value between [0.0,1.0], so a value of
+ * 0.2f means to set the alpha of an item to 20% of its normal value.
+ *
+ * @param minElementHeight The minimum element height as a ratio of the viewport size to use
+ * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+ * that a given content item will start to be scaled. Items smaller than [minElementHeight]
+ * will be treated as if [minElementHeight]. Must be less than or equal to [maxElementHeight].
+ *
+ * @param maxElementHeight The maximum element height as a ratio of the viewport size to use
+ * for determining the transition point within ([minTransitionArea], [maxTransitionArea])
+ * that a given content item will start to be scaled. Items larger than [maxElementHeight]
+ * will be treated as if [maxElementHeight]. Must be greater than or equal to
+ * [minElementHeight].
+ *
+ * @param minTransitionArea The lower bound of the scaling transition area, closest to the
+ * edge of the component. Defined as a ratio of the distance between the viewport center line
+ * and viewport edge of the list component. Must be less than or equal to [maxTransitionArea].
+ *
+ * @param maxTransitionArea The upper bound of the scaling transition area, closest to the
+ * center of the component. The is a ratio of the distance between the viewport center line and
+ * viewport edge of the list component. Must be greater
+ * than or equal to [minTransitionArea].
+ *
+ * @param scaleInterpolator An interpolator to use to determine how to apply scaling as a
+ * item transitions across the scaling transition area.
+ *
+ * @param viewportVerticalOffsetResolver The additional padding to consider above and below the
+ * viewport of a [ScalingLazyColumn] when considering which items to draw in the viewport. If
+ * set to 0 then no additional padding will be provided and only the items which would appear
+ * in the viewport before any scaling is applied will be considered for drawing, this may
+ * leave blank space at the top and bottom of the viewport where the next available item
+ * could have been drawn once other items have been scaled down in size. The larger this
+ * value is set to will allow for more content items to be considered for drawing in the
+ * viewport, however there is a performance cost associated with materializing items that are
+ * subsequently not drawn. The higher/more extreme the scaling parameters that are applied to
+ * the [ScalingLazyColumn] the more padding may be needed to ensure there are always enough
+ * content items available to be rendered. By default will be 20% of the maxHeight of the
+ * viewport above and below the content.
+ */
+ fun scalingParams(
+ edgeScale: Float = 0.5f,
+ edgeAlpha: Float = 0.5f,
+ minElementHeight: Float = 0.2f,
+ maxElementHeight: Float = 0.8f,
+ minTransitionArea: Float = 0.2f,
+ maxTransitionArea: Float = 0.6f,
+ scaleInterpolator: Easing = CubicBezierEasing(0.25f, 0.00f, 0.75f, 1.00f),
+ viewportVerticalOffsetResolver: (Constraints) -> Int = { (it.maxHeight / 5f).toInt() }
+ ): ScalingParams = DefaultScalingParams(
+ edgeScale = edgeScale,
+ edgeAlpha = edgeAlpha,
+ minElementHeight = minElementHeight,
+ maxElementHeight = maxElementHeight,
+ minTransitionArea = minTransitionArea,
+ maxTransitionArea = maxTransitionArea,
+ scaleInterpolator = scaleInterpolator,
+ viewportVerticalOffsetResolver = viewportVerticalOffsetResolver
+ )
+}
+
+private class ScalingLazyColumnScopeImpl(
+ private val state: LazyListState,
+ private val scope: LazyListScope,
+ private val scalingParams: ScalingParams,
+ private val realViewportSize: Dp,
+ private val paddingBetweenItems: Dp
+) : ScalingLazyColumnScope {
+
+ private var currentStartIndex = 0
+
+ override fun item(content: @Composable () -> Unit) {
+ val startIndex = currentStartIndex
+ scope.item {
+ ScalingLazyColumnItemWrapper(
+ startIndex,
+ state,
+ scalingParams,
+ realViewportSize,
+ paddingBetweenItems,
+ content = content
+ )
+ }
+ currentStartIndex++
+ }
+
+ override fun items(count: Int, itemContent: @Composable (Int) -> Unit) {
+ val startIndex = currentStartIndex
+ scope.items(count) {
+ ScalingLazyColumnItemWrapper(
+ startIndex + it,
+ state,
+ scalingParams,
+ realViewportSize,
+ paddingBetweenItems
+ ) {
+ itemContent(it)
+ }
+ }
+ currentStartIndex += count
+ }
+}
+
+@Composable
+private fun ScalingLazyColumnItemWrapper(
+ index: Int,
+ state: LazyListState,
+ scalingParams: ScalingParams,
+ realViewportSize: Dp,
+ paddingBetweenItems: Dp,
+ content: @Composable () -> Unit
+) {
+ Box(
+ // TODO (b/194464927): Refactor this method to make it more readable
+ Modifier.graphicsLayer {
+ val items = state.layoutInfo.visibleItemsInfo
+ val currentItem = items.find { it.index == index }
+ if (currentItem != null) {
+ val viewportSize = realViewportSize.roundToPx()
+ val centerOffset = viewportSize / 2
+ val paddingBetweenItemsPx = paddingBetweenItems.roundToPx()
+
+ val rawItemStart = currentItem.offset
+ val rawItemEnd = rawItemStart + currentItem.size
+ if (rawItemEnd < centerOffset) {
+ var currentSumOfScaledSizeDiffs = 0
+ items.reversed().forEach {
+ if (it.index > currentItem.index && it.offset < centerOffset) {
+ val (scale, _) = calculateScaleAndAlpha(
+ 0,
+ viewportSize,
+ it.offset + currentSumOfScaledSizeDiffs,
+ it.offset + it.size + currentSumOfScaledSizeDiffs,
+ scalingParams
+ )
+ currentSumOfScaledSizeDiffs += it.size -
+ (it.size * scale).roundToInt() +
+ (
+ paddingBetweenItemsPx -
+ (paddingBetweenItemsPx * scale).roundToInt()
+ )
+ }
+ }
+ translationY = currentSumOfScaledSizeDiffs.toFloat()
+ }
+ if (rawItemEnd > centerOffset) {
+ var currentSumOfScaledSizeDiffs = 0
+ items.forEach {
+ if (it.index < currentItem.index && it.offset > centerOffset) {
+ val (scale, _) = calculateScaleAndAlpha(
+ 0,
+ viewportSize,
+ it.offset + currentSumOfScaledSizeDiffs,
+ it.offset + it.size + currentSumOfScaledSizeDiffs,
+ scalingParams
+ )
+ currentSumOfScaledSizeDiffs -= it.size -
+ (it.size * scale).roundToInt() -
+ (
+ paddingBetweenItemsPx -
+ (paddingBetweenItemsPx * scale).roundToInt()
+ )
+ }
+ }
+ translationY = currentSumOfScaledSizeDiffs.toFloat()
+ }
+
+ val (scaleToApply, alphaToApply) = calculateScaleAndAlpha(
+ 0,
+ viewportSize,
+ rawItemStart + translationY.roundToInt(),
+ rawItemEnd + translationY.roundToInt(),
+ scalingParams
+ )
+
+ alpha = alphaToApply
+ scaleX = scaleToApply
+ scaleY = scaleToApply
+ val halfScaleSizeDiff = (currentItem.size - (currentItem.size * scaleToApply)) / 2f
+ if (rawItemEnd < centerOffset) {
+ translationY += halfScaleSizeDiff
+ }
+ if (rawItemEnd > centerOffset) {
+ translationY -= halfScaleSizeDiff
+ }
+ }
+ }
+ ) {
+ content()
+ }
+}
+
+@Immutable
+private class CombinedPaddingValues(
+ @Stable
+ val contentPadding: PaddingValues,
+ @Stable
+ val extraPadding: Dp
+) : PaddingValues {
+ override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp =
+ contentPadding.calculateLeftPadding(layoutDirection)
+
+ override fun calculateTopPadding(): Dp =
+ contentPadding.calculateTopPadding() + extraPadding
+
+ override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp =
+ contentPadding.calculateRightPadding(layoutDirection)
+
+ override fun calculateBottomPadding(): Dp =
+ contentPadding.calculateBottomPadding() + extraPadding
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null) return false
+ if (this::class != other::class) return false
+
+ other as CombinedPaddingValues
+
+ if (contentPadding != other.contentPadding) return false
+ if (extraPadding != other.extraPadding) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = contentPadding.hashCode()
+ result = 31 * result + extraPadding.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "CombinedPaddingValuesImpl(contentPadding=$contentPadding, " +
+ "extraPadding=$extraPadding)"
+ }
+}
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
new file mode 100644
index 0000000..401b349
--- /dev/null
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnMeasure.kt
@@ -0,0 +1,310 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material
+
+import androidx.compose.animation.core.Easing
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.unit.Constraints
+import kotlin.math.min
+
+/**
+ * Parameters to control the scaling of the contents of a [ScalingLazyColumn]. The contents
+ * of a [ScalingLazyColumn] are scaled down (made smaller) as they move further from the center
+ * of the viewport. This scaling gives a "fisheye" effect with the contents in the center being
+ * larger and therefore more prominent.
+ *
+ * Items in the center of the component's viewport will be full sized and with normal transparency.
+ * As items move further from the center of the viewport they get smaller and become transparent.
+ *
+ * The scaling parameters allow for larger items to start being scaled closer to the center than
+ * smaller items. This allows for larger items to scale over a bigger transition area giving a more
+ * visually pleasing effect.
+ *
+ * Scaling transitions take place between a transition line and the edge of the screen. The trigger
+ * for an item to start being scaled is when its most central edge, the item's edge that is furthest
+ * from the screen edge, passing across the item's transition line. The amount of scaling to apply is
+ * a function of the how far the item has moved through its transition area. An interpolator is
+ * applied to allow for the scaling to follow a bezier curve.
+ *
+ * There are 4 properties that are used to determine an item's scaling transition point.
+ *
+ * [maxTransitionArea] and [minTransitionArea] define the range in which all item scaling lines sit.
+ * The largest items will start to scale at the [maxTransitionArea] and the smallest items will
+ * start to scale at the [minTransitionArea].
+ *
+ * The [minTransitionArea] and [maxTransitionArea] apply either side of the center line of the
+ * viewport creating 2 transition areas one at the top/start the other at the bottom/end.
+ * So a [maxTransitionArea] value of 0.6f on a Viewport of size 320 dp will give start transition
+ * line for scaling at (320 / 2) * 0.6 = 96 dp from the top/start and bottom/end edges of the
+ * viewport. Similarly [minTransitionArea] gives the point at which the scaling transition area
+ * ends, e.g. a value of 0.2 with the same 320 dp screen gives an min scaling transition area line
+ * of (320 / 2) * 0.2 = 32 dp from top/start and bottom/end. So in this example we have two
+ * transition areas in the ranges [32.dp,96.dp] and [224.dp (320.dp-96.d),288.dp (320.dp-32.dp)].
+ *
+ * Deciding for a specific content item exactly where its transition line will be within the
+ * ([minTransitionArea], [maxTransitionArea]) transition area is determined by its height as a
+ * fraction of the viewport height and the properties [minElementHeight] and [maxElementHeight],
+ * also defined as a fraction of the viewport height.
+ *
+ * If an item is smaller than [minElementHeight] it is treated as is [minElementHeight] and if
+ * larger than [maxElementHeight] then it is treated as if [maxElementHeight].
+ *
+ * Given the size of an item where it sits between [minElementHeight] and [maxElementHeight] is used
+ * to determine what fraction of the transition area to use. For example if [minElementHeight] is
+ * 0.2 and [maxElementHeight] is 0.8 then a component item that is 0.4 (40%) of the size of the
+ * viewport would start to scale when it was 0.333 (33.3%) of the way through the transition area,
+ * (0.4 - 0.2) / (0.8 - 0.2) = 0.2 / 0.6 = 0.333.
+ *
+ * Taking the example transition area above that means that the scaling line for the item would be a
+ * third of the way between 32.dp and 96.dp. 32.dp + ((96.dp-32.dp) * 0.333) = 53.dp. So this item
+ * would start to scale when it moved from the center across the 53.dp line and its scaling would be
+ * between 53.dp and 0.dp.
+ */
+public interface ScalingParams {
+ /**
+ * What fraction of the full size of the item to scale it by when most
+ * scaled, e.g. at the viewport edge. A value between [0.0,1.0], so a value of 0.2f means
+ * to scale an item to 20% of its normal size.
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val edgeScale: Float
+
+ /**
+ * What fraction of the full transparency of the item to scale it by when
+ * most scaled, e.g. at the viewport edge. A value between [0.0,1.0], so a value of 0.2f
+ * means to set the alpha of an item to 20% of its normal value.
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val edgeAlpha: Float
+
+ /**
+ * The minimum element height as a fraction of the viewport size to use
+ * for determining the transition point within ([minTransitionArea], [maxTransitionArea]) that a
+ * given content item will start to be scaled. Items smaller than [minElementHeight] will be
+ * treated as if [minElementHeight]. Must be less than or equal to [maxElementHeight].
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val minElementHeight: Float
+
+ /**
+ * The minimum element height as a fraction of the viewport size to use
+ * for determining the transition point within ([minTransitionArea], [maxTransitionArea]) that a
+ * given content item will start to be scaled. Items smaller than [minElementHeight] will be
+ * treated as if [minElementHeight]. Must be less than or equal to [maxElementHeight].
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val maxElementHeight: Float
+
+ /**
+ * The lower bound of the scaling transition area, closest to the edge
+ * of the component. Defined as a fraction of the distance between the viewport center line and
+ * viewport edge of the component. Must be less than or equal to [maxTransitionArea].
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val minTransitionArea: Float
+
+ /**
+ * The upper bound of the scaling transition area, closest to the center
+ * of the component. Defined as a fraction of the distance between the viewport center line and
+ * viewport edge of the component. Must be greater
+ * than or equal to [minTransitionArea].
+ */
+// @FloatRange(
+// fromInclusive = true, from = 0.0, toInclusive = true, to = 1.0
+// )
+ val maxTransitionArea: Float
+
+ /**
+ * An interpolator to use to determine how to apply scaling as a item
+ * transitions across the scaling transition area.
+ */
+ val scaleInterpolator: Easing
+
+ /**
+ * Determine the offset/extra padding (in pixels) that is used to define a space for additional
+ * items that should be considered for drawing on the screen as the scaling of the visible
+ * items in viewport is calculated. This additional padding area means that more items will
+ * materialized and therefore be in scope for being drawn in the viewport if the scaling of
+ * other elements means that there is additional space at the top and bottom of the viewport
+ * that can be used. The default value is a fifth of the viewport height allowing an
+ * additional 20% of the viewport height above and below the viewport.
+ *
+ * @param viewportConstraints the viewports constraints
+ */
+ public fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int
+}
+
+internal class DefaultScalingParams(
+ override val edgeScale: Float,
+ override val edgeAlpha: Float,
+ override val minElementHeight: Float,
+ override val maxElementHeight: Float,
+ override val minTransitionArea: Float,
+ override val maxTransitionArea: Float,
+ override val scaleInterpolator: Easing,
+ val viewportVerticalOffsetResolver: (Constraints) -> Int,
+) : ScalingParams {
+
+ init {
+ check(
+ minElementHeight <= maxElementHeight,
+ { "minElementHeight must be less than or equal to maxElementHeight" }
+ )
+ check(
+ minTransitionArea <= maxTransitionArea,
+ { "minTransitionArea must be less than or equal to maxTransitionArea" }
+ )
+ }
+
+ override fun resolveViewportVerticalOffset(viewportConstraints: Constraints): Int {
+ return viewportVerticalOffsetResolver(viewportConstraints)
+ }
+
+ override fun toString(): String {
+ return "ScalingParams(edgeScale=$edgeScale, edgeAlpha=$edgeAlpha, " +
+ "minElementHeight=$minElementHeight, maxElementHeight=$maxElementHeight, " +
+ "minTransitionArea=$minTransitionArea, maxTransitionArea=$maxTransitionArea)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null) return false
+ if (this::class != other::class) return false
+
+ other as DefaultScalingParams
+
+ if (edgeScale != other.edgeScale) return false
+ if (edgeAlpha != other.edgeAlpha) return false
+ if (minElementHeight != other.minElementHeight) return false
+ if (maxElementHeight != other.maxElementHeight) return false
+ if (minTransitionArea != other.minTransitionArea) return false
+ if (maxTransitionArea != other.maxTransitionArea) return false
+ if (scaleInterpolator != other.scaleInterpolator) return false
+ if (viewportVerticalOffsetResolver != other.viewportVerticalOffsetResolver) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = edgeScale.hashCode()
+ result = 31 * result + edgeAlpha.hashCode()
+ result = 31 * result + minElementHeight.hashCode()
+ result = 31 * result + maxElementHeight.hashCode()
+ result = 31 * result + minTransitionArea.hashCode()
+ result = 31 * result + maxTransitionArea.hashCode()
+ result = 31 * result + scaleInterpolator.hashCode()
+ result = 31 * result + viewportVerticalOffsetResolver.hashCode()
+ return result
+ }
+}
+
+/**
+ * Calculate the scale and alpha to apply for an item based on the start and end position of the
+ * component's viewport in pixels and top and bottom position of the item, also in pixels.
+ *
+ * Firstly worked out if the component is above or below the viewport's center-line which determines
+ * whether the item's top or bottom will be used as the trigger for scaling/alpha.
+ *
+ * Uses the scalingParams to determine where the scaling transition line is for the component.
+ *
+ * Then determines if the component is inside the scaling area, and if so what scaling/alpha effects
+ * to apply.
+ *
+ * @param viewPortStartPx The start position of the component's viewport in pixels
+ * @param viewPortEndPx The end position of the component's viewport in pixels
+ * @param itemTopPx The top of the content item in pixels.
+ * @param itemBottomPx The bottom of the content item in pixels.
+ * @param scalingParams The parameters that determine where the item's scaling transition line
+ * is, how scaling and transparency to apply.
+ */
+internal fun calculateScaleAndAlpha(
+ viewPortStartPx: Int,
+ viewPortEndPx: Int,
+ itemTopPx: Int,
+ itemBottomPx: Int,
+ scalingParams: ScalingParams,
+): ScaleAndAlpha {
+ var scaleToApply = 1.0f
+ var alphaToApply = 1.0f
+
+ val viewPortHeightPx = (viewPortEndPx - viewPortStartPx).toFloat()
+ val itemHeightPx = (itemBottomPx - itemTopPx).toFloat()
+
+ /*
+ * Calculate the position of the edge of the item closest to the center line of the viewport as
+ * a fraction of the viewport. The [itemEdgePx] and [scrollPositionPx] values are in pixels.
+ */
+ val itemEdgeAsFractionOfViewport =
+ min(itemBottomPx - viewPortStartPx, viewPortEndPx - itemTopPx) / viewPortHeightPx
+
+ val heightAsFractionOfViewPort = itemHeightPx / viewPortHeightPx
+ if (itemEdgeAsFractionOfViewport > 0.0f && itemEdgeAsFractionOfViewport < 1.0f) {
+ // Work out the scaling line based on size, this is a value between 0.0..1.0
+ val sizeRatio: Float =
+ (
+ (heightAsFractionOfViewPort - scalingParams.minElementHeight) /
+ (scalingParams.maxElementHeight - scalingParams.minElementHeight)
+ ).coerceIn(0f, 1f)
+
+ val scalingLineAsFractionOfViewPort =
+ scalingParams.minTransitionArea +
+ (scalingParams.maxTransitionArea - scalingParams.minTransitionArea) *
+ sizeRatio
+
+ if (itemEdgeAsFractionOfViewport < scalingLineAsFractionOfViewPort) {
+ // We are scaling
+ val fractionOfDiffToApplyRaw =
+ (scalingLineAsFractionOfViewPort - itemEdgeAsFractionOfViewport) /
+ scalingLineAsFractionOfViewPort
+ val fractionOfDiffToApplyInterpolated =
+ scalingParams.scaleInterpolator.transform(fractionOfDiffToApplyRaw)
+
+ scaleToApply =
+ scalingParams.edgeScale +
+ (1.0f - scalingParams.edgeScale) *
+ (1.0f - fractionOfDiffToApplyInterpolated)
+ alphaToApply =
+ scalingParams.edgeAlpha +
+ (1.0f - scalingParams.edgeAlpha) *
+ (1.0f - fractionOfDiffToApplyInterpolated)
+ }
+ } else {
+ scaleToApply = scalingParams.edgeScale
+ alphaToApply = scalingParams.edgeAlpha
+ }
+
+ return ScaleAndAlpha(scaleToApply, alphaToApply)
+}
+
+@Immutable
+internal data class ScaleAndAlpha(
+ val scale: Float,
+ val alpha: Float
+)
+
+@Immutable
+internal data class ScaleAlphaAndStartPosition(val scale: Float, val alpha: Float, val top: Int)
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnState.kt
new file mode 100644
index 0000000..738ed4d
--- /dev/null
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyColumnState.kt
@@ -0,0 +1,52 @@
+package androidx.wear.compose.material
+
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+
+/**
+ * Creates a [ScalingLazyColumnState] that is remembered across compositions.
+ */
+@Composable
+public fun rememberScalingLazyColumnState(): ScalingLazyColumnState {
+ return rememberSaveable(saver = ScalingLazyColumnState.Saver) {
+ ScalingLazyColumnState()
+ }
+}
+
+/**
+ * A state object that can be hoisted to control and observe scrolling.
+ * TODO (b/193792848): Add scrolling and snap support.
+ *
+ * In most cases, this will be created via [rememberScalingLazyColumnState].
+ */
+@Stable
+public class ScalingLazyColumnState {
+
+ internal var lazyListState: LazyListState = LazyListState(0, 0)
+
+ companion object {
+ /**
+ * The default [Saver] implementation for [ScalingLazyColumnState].
+ */
+ val Saver: Saver<ScalingLazyColumnState, *> = listSaver(
+ save = {
+ listOf(
+ it.lazyListState.firstVisibleItemIndex,
+ it.lazyListState.firstVisibleItemScrollOffset
+ )
+ },
+ restore = {
+ var scalingLazyColumnState = ScalingLazyColumnState()
+ scalingLazyColumnState.lazyListState = LazyListState(
+ firstVisibleItemIndex = it[0],
+ firstVisibleItemScrollOffset = it[1]
+ )
+ scalingLazyColumnState
+ }
+ )
+ }
+}
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 84713d9..6ddb2f4 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -57,6 +57,7 @@
implementation(project(':wear:compose:compose-material'))
implementation(project(':wear:compose:compose-foundation'))
implementation(project(":wear:compose:compose-foundation-samples"))
+ implementation(project(":wear:compose:compose-material-samples"))
androidTestImplementation(project(":compose:ui:ui-test-junit4"))
androidTestImplementation(libs.testCore)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
index a480cb27..b6c6260 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/CardDemo.kt
@@ -130,7 +130,7 @@
Text("Text coloured to stand out on the image")
}
},
- backgroundPainter = CardDefaults.imageBackgroundPainter(
+ backgroundPainter = CardDefaults.imageWithScrimBackgroundPainter(
backgroundImagePainter = painterResource(id = R.drawable.backgroundimage1)
),
bodyColor = MaterialTheme.colors.onSurface,
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index 7f313ed..798cb59 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -18,6 +18,8 @@
import androidx.compose.integration.demos.common.ComposableDemo
import androidx.compose.integration.demos.common.DemoCategory
+import androidx.wear.compose.material.samples.ScalingLazyColumnWithHeaders
+import androidx.wear.compose.material.samples.SimpleScalingLazyColumn
val WearMaterialDemos = DemoCategory(
"Material",
@@ -49,5 +51,7 @@
)
),
ComposableDemo("Card") { CardDemo() },
+ ComposableDemo("Scaling Lazy Column") { SimpleScalingLazyColumn() },
+ ComposableDemo("List Headers") { ScalingLazyColumnWithHeaders() },
),
)
diff --git a/wear/wear-phone-interactions/build.gradle b/wear/wear-phone-interactions/build.gradle
index 4b02455..77907c0 100644
--- a/wear/wear-phone-interactions/build.gradle
+++ b/wear/wear-phone-interactions/build.gradle
@@ -38,6 +38,7 @@
testImplementation(libs.robolectric)
testImplementation(libs.mockitoCore)
testImplementation(libs.mockitoKotlin)
+ testImplementation(libs.truth)
}
android {
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
index 7426ffc..2590363 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/OAuthRequest.kt
@@ -148,10 +148,11 @@
"redirect_uri",
Uri.withAppendedPath(
if (redirectUrl == null) {
- if (WearTypeHelper.isChinaBuild(context))
+ if (WearTypeHelper.isChinaBuild(context)) {
Uri.parse(WEAR_REDIRECT_URL_PREFIX_CN)
- else
+ } else {
Uri.parse(WEAR_REDIRECT_URL_PREFIX)
+ }
} else {
redirectUrl
},
@@ -178,14 +179,14 @@
expectedQueryParam: String
) {
val currentQueryParam = authProviderUrl!!.getQueryParameter(queryKey)
- currentQueryParam?.let {
+ if (currentQueryParam != null) {
require(expectedQueryParam == currentQueryParam) {
"The '$queryKey' query param already exists in the authProviderUrl, " +
"expect to have the value of '$expectedQueryParam', but " +
"'$currentQueryParam' is given. Please correct it, or leave it out " +
"to allow the request builder to append it automatically."
}
- } ?: run {
+ } else {
requestUriBuilder.appendQueryParameter(queryKey, expectedQueryParam)
}
}
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
index 130e3a8..093ca7c 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
@@ -23,7 +23,7 @@
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
import androidx.wear.phone.interactions.PhoneTypeHelper.Companion.getPhoneDeviceType
-import org.junit.Assert.assertEquals
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -61,36 +61,32 @@
@Test
fun testGetDeviceType_returnsIosWhenAltMode() {
createFakePhoneTypeQuery(PhoneTypeHelper.IOS_MODE)
- assertEquals(
- getPhoneDeviceType(ApplicationProvider.getApplicationContext()).toLong(),
- PhoneTypeHelper.DEVICE_TYPE_IOS.toLong()
- )
+ assertThat(
+ getPhoneDeviceType(ApplicationProvider.getApplicationContext())
+ ).isEqualTo(PhoneTypeHelper.DEVICE_TYPE_IOS)
}
@Test
fun testGetDeviceType_returnsAndroidWhenNonAltMode() {
createFakePhoneTypeQuery(PhoneTypeHelper.ANDROID_MODE)
- assertEquals(
- getPhoneDeviceType(ApplicationProvider.getApplicationContext()).toLong(),
- PhoneTypeHelper.DEVICE_TYPE_ANDROID.toLong()
- )
+ assertThat(
+ getPhoneDeviceType(ApplicationProvider.getApplicationContext())
+ ).isEqualTo(PhoneTypeHelper.DEVICE_TYPE_ANDROID)
}
@Test
fun testGetDeviceType_returnsErrorWhenModeUnknown() {
createFakePhoneTypeQuery(PhoneTypeHelper.DEVICE_TYPE_UNKNOWN)
- assertEquals(
- getPhoneDeviceType(ApplicationProvider.getApplicationContext()).toLong(),
- PhoneTypeHelper.DEVICE_TYPE_UNKNOWN.toLong()
- )
+ assertThat(
+ getPhoneDeviceType(ApplicationProvider.getApplicationContext())
+ ).isEqualTo(PhoneTypeHelper.DEVICE_TYPE_UNKNOWN)
}
@Test
fun testGetDeviceType_returnsErrorWhenContentMissing() {
- assertEquals(
- getPhoneDeviceType(ApplicationProvider.getApplicationContext()).toLong(),
- PhoneTypeHelper.DEVICE_TYPE_ERROR.toLong()
- )
+ assertThat(
+ getPhoneDeviceType(ApplicationProvider.getApplicationContext())
+ ).isEqualTo(PhoneTypeHelper.DEVICE_TYPE_ERROR)
}
/*
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
index e92e749..9de98e3 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/CodeVerifierCodeChallengeTest.kt
@@ -16,24 +16,27 @@
package androidx.wear.phone.interactions.authentication
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
+import org.robolectric.annotation.Config
/** Unit tests for [CodeVerifier] and [CodeChallenge] */
+@Config(minSdk = 26)
+@RequiresApi(Build.VERSION_CODES.O)
public class CodeVerifierCodeChallengeTest {
@Test
public fun testVerifierDefaultConstructor() {
val verifier = CodeVerifier()
- assertEquals(43, verifier.value.length)
+ assertThat(verifier.value.length).isEqualTo(43)
}
@Test
public fun testVerifierConstructor() {
val verifier = CodeVerifier(96)
- assertEquals(128, verifier.value.length)
+ assertThat(verifier.value.length).isEqualTo(128)
}
@Test
@@ -49,14 +52,14 @@
@Test
public fun testVerifierEquality() {
val verifier = CodeVerifier()
- assertTrue(verifier.equals(CodeVerifier(verifier.value)))
+ assertThat(CodeVerifier(verifier.value)).isEqualTo(verifier)
}
@Test
public fun testVerifierInequality() {
- assertFalse(CodeVerifier().equals(CodeVerifier()))
- assertFalse(CodeVerifier(50).equals(CodeVerifier(50)))
- assertFalse(CodeVerifier().equals(null))
+ assertThat(CodeVerifier()).isNotEqualTo(CodeVerifier())
+ assertThat(CodeVerifier(50)).isNotEqualTo(CodeVerifier(50))
+ assertThat(CodeVerifier()).isNotEqualTo(null)
}
@Test
@@ -64,22 +67,20 @@
// see https://tools.ietf.org/html/rfc7636#appendix-A
val verifier = CodeVerifier("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
val challenge = CodeChallenge(verifier)
- assertEquals("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM", challenge.value)
+ assertThat(challenge.value).isEqualTo("E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM")
}
@Test
public fun testChallengeEquality() {
val verifierValue = "jdshfkshg-8973834_SDFSSGE"
- assertTrue(
- CodeChallenge(CodeVerifier(verifierValue)).equals(
- CodeChallenge(CodeVerifier(verifierValue))
- )
- )
+ assertThat(
+ CodeChallenge(CodeVerifier(verifierValue))
+ ).isEqualTo(CodeChallenge(CodeVerifier(verifierValue)))
}
@Test
public fun testChallengeInequality() {
- assertFalse(CodeChallenge(CodeVerifier()).equals(CodeChallenge(CodeVerifier())))
- assertFalse(CodeChallenge(CodeVerifier()).equals(null))
+ assertThat(CodeChallenge(CodeVerifier())).isNotEqualTo(CodeChallenge(CodeVerifier()))
+ assertThat(CodeChallenge(CodeVerifier())).isNotEqualTo(null)
}
}
\ No newline at end of file
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
index 8a2701c..3568a44 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/OAuthRequestResponseTest.kt
@@ -22,8 +22,7 @@
import androidx.annotation.RequiresApi
import androidx.test.core.app.ApplicationProvider
import androidx.wear.phone.interactions.WearPhoneInteractionsTestRunner
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
@@ -58,7 +57,7 @@
fun setUp() {
// We need to make sure that context.packageName is not empty as it can lead to passing
// tests when they shouldn't.
- assertFalse(context.packageName.isEmpty())
+ assertThat(context.packageName).isNotEmpty()
}
private fun setSystemFeatureChina(value: Boolean) {
@@ -80,12 +79,12 @@
}
val requestUrl = request!!.requestUrl
- assertEquals(requestUrl.toString().indexOf(expectedAuthProviderUrl), 0)
- assertEquals(requestUrl.getQueryParameter("redirect_uri"), expectedRedirectUri)
- assertEquals(requestUrl.getQueryParameter("client_id"), expectedClientId)
- assertEquals(requestUrl.getQueryParameter("response_type"), "code")
- assertEquals(requestUrl.getQueryParameter("code_challenge"), expectedCodeChallenge)
- assertEquals(requestUrl.getQueryParameter("code_challenge_method"), "S256")
+ assertThat(requestUrl.toString().indexOf(expectedAuthProviderUrl)).isEqualTo(0)
+ assertThat(requestUrl.getQueryParameter("redirect_uri")).isEqualTo(expectedRedirectUri)
+ assertThat(requestUrl.getQueryParameter("client_id")).isEqualTo(expectedClientId)
+ assertThat(requestUrl.getQueryParameter("response_type")).isEqualTo("code")
+ assertThat(requestUrl.getQueryParameter("code_challenge")).isEqualTo(expectedCodeChallenge)
+ assertThat(requestUrl.getQueryParameter("code_challenge_method")).isEqualTo("S256")
}
private fun checkBuildFailure(builder: OAuthRequest.Builder, errorMsg: String) {
@@ -93,7 +92,7 @@
builder.build()
fail("should fail without providing correct/adequate info for building request")
} catch (e: Exception) {
- assertEquals(errorMsg, e.message)
+ assertThat(e.message).isEqualTo(errorMsg)
}
}
@@ -418,8 +417,8 @@
public fun testNoErrorResponseBuild() {
val response = OAuthResponse.Builder().setResponseUrl(responseUrl).build()
- assertEquals(RemoteAuthClient.NO_ERROR, response.errorCode)
- assertEquals(responseUrl, response.responseUrl)
+ assertThat(response.errorCode).isEqualTo(RemoteAuthClient.NO_ERROR)
+ assertThat(response.responseUrl).isEqualTo(responseUrl)
}
@Test
@@ -428,14 +427,14 @@
.setErrorCode(RemoteAuthClient.ERROR_UNSUPPORTED)
.build()
- assertEquals(RemoteAuthClient.ERROR_UNSUPPORTED, response1.errorCode)
- assertEquals(null, response1.responseUrl)
+ assertThat(response1.errorCode).isEqualTo(RemoteAuthClient.ERROR_UNSUPPORTED)
+ assertThat(response1.responseUrl).isNull()
val response2 = OAuthResponse.Builder()
.setErrorCode(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE)
.build()
- assertEquals(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE, response2.errorCode)
- assertEquals(null, response2.responseUrl)
+ assertThat(response2.errorCode).isEqualTo(RemoteAuthClient.ERROR_PHONE_UNAVAILABLE)
+ assertThat(response2.responseUrl).isNull()
}
}
\ No newline at end of file
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
index ed0b6dc..2d0b389 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/authentication/RemoteAuthTest.kt
@@ -21,25 +21,31 @@
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
+import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import android.util.Pair
+import androidx.annotation.RequiresApi
import androidx.test.core.app.ApplicationProvider
import androidx.wear.phone.interactions.WearPhoneInteractionsTestRunner
-import org.junit.Assert
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.robolectric.Shadows
+import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
-import java.util.ArrayList
import java.util.concurrent.Executor
/** Unit tests for [RemoteAuthClient]. */
@RunWith(WearPhoneInteractionsTestRunner::class)
@DoNotInstrument // Needed because it is defined in the "android" package.
+@Config(minSdk = 26)
+@RequiresApi(Build.VERSION_CODES.O)
public class RemoteAuthTest {
+ @Config(minSdk = 26)
+ @RequiresApi(Build.VERSION_CODES.O)
internal companion object {
private val context: Context = ApplicationProvider.getApplicationContext()
private val shadowPackageManager = Shadows.shadowOf(context.packageManager)
@@ -67,6 +73,7 @@
.setCodeChallenge(CodeChallenge(CodeVerifier()))
.build()
}
+
private val response = OAuthResponse.Builder().setResponseUrl(responseUrl).build()
// Note: This can't be static as Robolectric isn't set up at class init time.
@@ -91,7 +98,7 @@
public fun doesntConnectUntilARequestIsMade() {
// WHEN the client is created
// THEN the Auth library should not yet connect to Clockwork Home
- Assert.assertEquals(ConnectionState.DISCONNECTED, fakeServiceBinder.state)
+ assertThat(fakeServiceBinder.state).isEqualTo(ConnectionState.DISCONNECTED)
}
@Test
@@ -108,7 +115,7 @@
mockCallback
)
// THEN a connection is made to Clockwork Home's Auth service
- Assert.assertEquals(ConnectionState.CONNECTING, fakeServiceBinder.state)
+ assertThat(fakeServiceBinder.state).isEqualTo(ConnectionState.CONNECTING)
}
@Test
@@ -120,14 +127,8 @@
val request = fakeService.requests[0]
val requestReceived = request.first
// THEN the request url is set correctly
- Assert.assertEquals(
- requestA.requestUrl,
- requestReceived.requestUrl
- )
- Assert.assertEquals(
- requestReceived.requestUrl.toString().indexOf(authProviderUrlA),
- 0
- )
+ assertThat(requestReceived.requestUrl).isEqualTo(requestA.requestUrl)
+ assertThat(requestReceived.requestUrl.toString().indexOf(authProviderUrlA)).isEqualTo(0)
}
@Test
@@ -140,24 +141,12 @@
// THEN two requests are made to Clockwork Home
val requestAReceived = fakeService.requests[0].first
val requestBReceived = fakeService.requests[1].first
- Assert.assertEquals(2, fakeService.requests.size.toLong())
+ assertThat(fakeService.requests.size.toLong()).isEqualTo(2)
// THEN the request url is set correctly for both (A then B)
- Assert.assertEquals(
- requestA.requestUrl,
- requestAReceived.requestUrl
- )
- Assert.assertEquals(
- requestB.requestUrl,
- requestBReceived.requestUrl
- )
- Assert.assertEquals(
- requestAReceived.requestUrl.toString().indexOf(authProviderUrlA),
- 0
- )
- Assert.assertEquals(
- requestBReceived.requestUrl.toString().indexOf(authProviderUrlB),
- 0
- )
+ assertThat(requestAReceived.requestUrl).isEqualTo(requestA.requestUrl)
+ assertThat(requestBReceived.requestUrl).isEqualTo(requestB.requestUrl)
+ assertThat(requestAReceived.requestUrl.toString().indexOf(authProviderUrlA)).isEqualTo(0)
+ assertThat(requestBReceived.requestUrl.toString().indexOf(authProviderUrlB)).isEqualTo(0)
}
@Test
@@ -187,7 +176,7 @@
)
// THEN the service remains connected (as there's still a request ongoing, and we won't get
// the callback for the other request if we unbind now)
- Assert.assertEquals(ConnectionState.CONNECTED, fakeServiceBinder.state)
+ assertThat(fakeServiceBinder.state).isEqualTo(ConnectionState.CONNECTED)
}
@Test
@@ -208,10 +197,10 @@
fakeService.requests[1].second
)
// THEN the OAuth library disconnects from Clockwork Home
- Assert.assertEquals(ConnectionState.DISCONNECTED, fakeServiceBinder.state)
+ assertThat(fakeServiceBinder.state).isEqualTo(ConnectionState.DISCONNECTED)
}
- private enum class ConnectionState {
+ internal enum class ConnectionState {
DISCONNECTED, CONNECTING, CONNECTED
}
@@ -237,7 +226,7 @@
}
fun completeConnection() {
- Assert.assertTrue(state == ConnectionState.CONNECTING)
+ assertThat(ConnectionState.CONNECTING).isEqualTo(state)
state = ConnectionState.CONNECTED
serviceConnection!!.onServiceConnected(mServiceName, fakeService.onBind(Intent()))
}
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceTest.kt
index 7100b14..4c765e2 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerServiceTest.kt
@@ -21,9 +21,7 @@
import android.support.wearable.notifications.IBridgingManagerService
import androidx.test.core.app.ApplicationProvider
import androidx.wear.phone.interactions.WearPhoneInteractionsTestRunner
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
+import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
@@ -60,12 +58,12 @@
val binder = bridgingManagerService.onBind(intent)
- assertNotNull(binder)
+ assertThat(binder).isNotNull()
val bridgingManagerServiceImpl = IBridgingManagerService.Stub.asInterface(binder)
- assertNotNull(bridgingManagerServiceImpl)
+ assertThat(bridgingManagerServiceImpl).isNotNull()
bridgingManagerServiceImpl.setBridgingConfig(bridgingConfig.toBundle(context))
- assertEquals(bridgingConfig, testBridgingConfigurationHandler.bridgingConfig)
+ assertThat(testBridgingConfigurationHandler.bridgingConfig).isEqualTo(bridgingConfig)
}
@Test
@@ -79,11 +77,11 @@
context /* packageName = PACKAGE_NAME */, false
).build()
- var binder = bridgingManagerService.onBind(intent)
+ val binder = bridgingManagerService.onBind(intent)
- assertNotNull(binder)
+ assertThat(binder).isNotNull()
val bridgingManagerServiceImpl = IBridgingManagerService.Stub.asInterface(binder)
- assertNotNull(bridgingManagerServiceImpl)
+ assertThat(bridgingManagerServiceImpl).isNotNull()
assertThrows(
IllegalArgumentException::class.java
@@ -100,7 +98,7 @@
val binder = bridgingManagerService.onBind(intent)
- assertNull(binder)
+ assertThat(binder).isNull()
}
@Test
@@ -110,7 +108,7 @@
val binder = bridgingManagerService.onBind(intent = null)
- assertNull(binder)
+ assertThat(binder).isNull()
}
companion object {
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerTest.kt
index 8d2ab49..7946e50 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/notifications/BridgingManagerTest.kt
@@ -19,14 +19,13 @@
import android.content.Context
import android.content.pm.PackageManager
import androidx.wear.phone.interactions.WearPhoneInteractionsTestRunner
-import org.junit.Assert
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.robolectric.annotation.internal.DoNotInstrument
import java.util.Arrays
-import java.util.HashSet
/** Unit tests for [BridgingManager]. */
@RunWith(WearPhoneInteractionsTestRunner::class)
@@ -48,12 +47,12 @@
mContext, false
).build()
- Assert.assertEquals(BridgingConfig(PACKAGE_NAME, false, HashSet()), bridgingConfig)
+ assertThat(bridgingConfig).isEqualTo(BridgingConfig(PACKAGE_NAME, false, HashSet()))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -62,12 +61,12 @@
mContext, true
).build()
- Assert.assertEquals(BridgingConfig(PACKAGE_NAME, true, HashSet()), bridgingConfig)
+ assertThat(bridgingConfig).isEqualTo(BridgingConfig(PACKAGE_NAME, true, HashSet()))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -76,12 +75,12 @@
mContext, true
).build()
- Assert.assertTrue(BridgingConfig(PACKAGE_NAME, true, HashSet()).equals(bridgingConfig))
+ assertThat(bridgingConfig).isEqualTo(BridgingConfig(PACKAGE_NAME, true, HashSet()))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -90,15 +89,14 @@
mContext, true
).addExcludedTag("foo").build()
- Assert.assertEquals(
- BridgingConfig(PACKAGE_NAME, true, HashSet(listOf("foo"))),
+ assertThat(
bridgingConfig
- )
+ ).isEqualTo(BridgingConfig(PACKAGE_NAME, true, HashSet(listOf("foo"))))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -111,15 +109,14 @@
.addExcludedTag("foo")
.build()
- Assert.assertEquals(
- BridgingConfig(PACKAGE_NAME, false, HashSet(Arrays.asList("foo", "bar"))),
+ assertThat(
bridgingConfig
- )
+ ).isEqualTo(BridgingConfig(PACKAGE_NAME, false, HashSet(Arrays.asList("foo", "bar"))))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -130,15 +127,14 @@
.addExcludedTags(Arrays.asList("foo", "bar", "foo"))
.build()
- Assert.assertEquals(
- BridgingConfig(PACKAGE_NAME, false, HashSet(Arrays.asList("foo", "bar"))),
+ assertThat(
bridgingConfig
- )
+ ).isEqualTo(BridgingConfig(PACKAGE_NAME, false, HashSet(Arrays.asList("foo", "bar"))))
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
@Test
@@ -153,17 +149,18 @@
.addExcludedTag("foo")
.build()
- Assert.assertEquals(
+ assertThat(
+ bridgingConfig
+ ).isEqualTo(
BridgingConfig(
PACKAGE_NAME, false, HashSet(Arrays.asList("foo", "bar", "123", "aaa", "abc"))
- ),
- bridgingConfig
+ )
)
// Test that conversion to and from bundle works as expected.
- Assert.assertEquals(
- bridgingConfig, BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
- )
+ assertThat(
+ BridgingConfig.fromBundle(bridgingConfig.toBundle(mContext))
+ ).isEqualTo(bridgingConfig)
}
private companion object {
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
index feddeee..576ad34 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionTest.kt
@@ -136,7 +136,7 @@
internal val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
internal val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
-internal val blueStyleOption = ListOption(Option.Id("bluestyle"), "Blue", icon = null)
+internal val blueStyleOption = ListOption(Option.Id("blue_style"), "Blue", icon = null)
internal val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
internal val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
UserStyleSetting.Id("color_style_setting"),
diff --git a/wear/wear-watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java b/wear/wear-watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
index 3892a05..aeef072 100644
--- a/wear/wear-watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
+++ b/wear/wear-watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
@@ -16,11 +16,8 @@
package androidx.wear.watchface.samples.minimal.complications;
-import android.content.Context;
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
@@ -29,6 +26,7 @@
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
import androidx.wear.complications.ComplicationDataSourceInfo;
import androidx.wear.complications.data.ComplicationData;
import androidx.wear.watchface.complications.rendering.ComplicationDrawable;
@@ -56,11 +54,7 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity_layout);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- mMainExecutor = MainExecutorApi28.getMainExecutor(this);
- } else {
- mMainExecutor = MainExecutorApi25.getMainExecutor();
- }
+ mMainExecutor = ContextCompat.getMainExecutor(getApplicationContext());
mComplicationProviderName = findViewById(R.id.complication_provider_name);
mComplicationPreview = findViewById(R.id.complication_preview);
@@ -177,19 +171,4 @@
private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
FutureCallback.addCallback(future, callback, mMainExecutor);
}
-
- private static final class MainExecutorApi25 {
- public static Handler sMainThreadHandler = new Handler(Looper.getMainLooper());
-
- public static Executor getMainExecutor() {
- return sMainThreadHandler::post;
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.P)
- private static final class MainExecutorApi28 {
- public static Executor getMainExecutor(Context context) {
- return context.getMainExecutor();
- }
- }
}
diff --git a/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java b/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
index 8aff71d..2241a0c 100644
--- a/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
+++ b/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
@@ -16,17 +16,13 @@
package androidx.wear.watchface.samples.minimal.style;
-import android.content.Context;
-import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import android.widget.TextView;
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
import androidx.wear.watchface.editor.ListenableEditorSession;
import androidx.wear.watchface.style.UserStyle;
import androidx.wear.watchface.style.UserStyleSetting;
@@ -58,11 +54,7 @@
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity_layout);
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- mMainExecutor = MainExecutorApi28.getMainExecutor(this);
- } else {
- mMainExecutor = MainExecutorApi25.getMainExecutor();
- }
+ mMainExecutor = ContextCompat.getMainExecutor(getApplicationContext());
mTimeStyle = new TimeStyle(this);
mStyleValue = findViewById(R.id.style_value);
@@ -136,19 +128,4 @@
map.put(TimeStyle.Value.SECONDS, TimeStyle.Value.MINIMAL);
return map;
}
-
- private static final class MainExecutorApi25 {
- public static Handler sMainThreadHandler = new Handler(Looper.getMainLooper());
-
- public static Executor getMainExecutor() {
- return sMainThreadHandler::post;
- }
- }
-
- @RequiresApi(Build.VERSION_CODES.P)
- private static final class MainExecutorApi28 {
- public static Executor getMainExecutor(Context context) {
- return context.getMainExecutor();
- }
- }
}
diff --git a/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/TimeStyle.java b/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/TimeStyle.java
index 9883bf3..db60362 100644
--- a/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/TimeStyle.java
+++ b/wear/wear-watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/TimeStyle.java
@@ -37,7 +37,7 @@
SECONDS,
}
- private static final UserStyleSetting.Id ID = new UserStyleSetting.Id("TypeStyle");
+ private static final UserStyleSetting.Id ID = new UserStyleSetting.Id("TimeStyle");
private static final UserStyleSetting.Option.Id MINIMAL_ID =
new UserStyleSetting.Option.Id("minimal");
@@ -102,12 +102,12 @@
new UserStyleSetting.ListUserStyleSetting.ListOption(
MINIMAL_ID,
getString(context, R.string.time_style_minimal_name),
- getIcon(context, R.drawable.tyme_style_minimal_icon)
+ getIcon(context, R.drawable.time_style_minimal_icon)
),
new UserStyleSetting.ListUserStyleSetting.ListOption(
SECONDS_ID,
getString(context, R.string.time_style_seconds_name),
- getIcon(context, R.drawable.tyme_style_seconds_icon)
+ getIcon(context, R.drawable.time_style_seconds_icon)
)
),
WatchFaceLayer.ALL_WATCH_FACE_LAYERS);
diff --git a/wear/wear-watchface-samples-minimal-style/src/main/res/drawable/tyme_style_minimal_icon.xml b/wear/wear-watchface-samples-minimal-style/src/main/res/drawable/time_style_minimal_icon.xml
similarity index 100%
rename from wear/wear-watchface-samples-minimal-style/src/main/res/drawable/tyme_style_minimal_icon.xml
rename to wear/wear-watchface-samples-minimal-style/src/main/res/drawable/time_style_minimal_icon.xml
diff --git a/wear/wear-watchface-samples-minimal-style/src/main/res/drawable/tyme_style_seconds_icon.xml b/wear/wear-watchface-samples-minimal-style/src/main/res/drawable/time_style_seconds_icon.xml
similarity index 100%
rename from wear/wear-watchface-samples-minimal-style/src/main/res/drawable/tyme_style_seconds_icon.xml
rename to wear/wear-watchface-samples-minimal-style/src/main/res/drawable/time_style_seconds_icon.xml
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index bb4a276..204bfa0 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -38,7 +38,7 @@
ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
private val blueStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
@@ -195,7 +195,7 @@
val userStyle = UserStyle(
UserStyleData(
mapOf(
- "color_style_setting" to "bluestyle".encodeToByteArray(),
+ "color_style_setting" to "blue_style".encodeToByteArray(),
"hand_style_setting" to "gothic_style".encodeToByteArray()
)
),
@@ -203,7 +203,7 @@
)
assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
- .isEqualTo("bluestyle")
+ .isEqualTo("blue_style")
assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
.isEqualTo("gothic_style")
}
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index 357ab40..aa3216b 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -70,7 +70,7 @@
icon = null
),
ListUserStyleSetting.ListOption(
- Option.Id("bluestyle"),
+ Option.Id("blue_style"),
"Blue",
icon = null
)
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 67ca8cf..b895934 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -132,7 +132,7 @@
ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
private val blueStyleOption =
- ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("blue_style"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
diff --git a/wear/wear/src/main/res/values-af/strings.xml b/wear/wear/src/main/res/values-af/strings.xml
index a0b1d41..a5e4ad237 100644
--- a/wear/wear/src/main/res/values-af/strings.xml
+++ b/wear/wear/src/main/res/values-af/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigasielaai"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Handelinglaai"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Sukses"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Mislukking"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Op jou foon oopgemaak"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Prent"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-am/strings.xml b/wear/wear/src/main/res/values-am/strings.xml
index e939f78..c0e7c27 100644
--- a/wear/wear/src/main/res/values-am/strings.xml
+++ b/wear/wear/src/main/res/values-am/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"የአሰሳ መሳቢያ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"የእርምጃ መሳቢያ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ስኬት"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"አልተሳካም"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"በስልክዎ ላይ ተከፍቷል"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ምስል"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ar/strings.xml b/wear/wear/src/main/res/values-ar/strings.xml
index 6f3fe90..21a9f90 100644
--- a/wear/wear/src/main/res/values-ar/strings.xml
+++ b/wear/wear/src/main/res/values-ar/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"لائحة التنقل"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"دُرج الإجراءات"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"تم تنفيذ الإجراء بنجاح."</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"تعذَّر إتمام العملية."</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"تم الفتح على هاتفك."</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"صورة"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-as/strings.xml b/wear/wear/src/main/res/values-as/strings.xml
index e905547..a0aa62c 100644
--- a/wear/wear/src/main/res/values-as/strings.xml
+++ b/wear/wear/src/main/res/values-as/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"নেভিগেশ্বন ড্ৰৱাৰ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"কাৰ্য ড্ৰৱাৰ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"সফল হৈছে"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"বিফল হৈছে"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"আপোনাৰ ফ’নত খোলা হৈছে"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"প্ৰতিচ্ছবি"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-az/strings.xml b/wear/wear/src/main/res/values-az/strings.xml
index 74ccfe8..8998fbf 100644
--- a/wear/wear/src/main/res/values-az/strings.xml
+++ b/wear/wear/src/main/res/values-az/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Naviqasiya qutusu"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Əməliyyat qutusu"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Uğurlu oldu"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Alınmadı"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Telefonunuzda açıldı"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Şəkil"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-b+sr+Latn/strings.xml b/wear/wear/src/main/res/values-b+sr+Latn/strings.xml
index c58868b..107f542 100644
--- a/wear/wear/src/main/res/values-b+sr+Latn/strings.xml
+++ b/wear/wear/src/main/res/values-b+sr+Latn/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Fioka za navigaciju"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Fioka za radnju"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Uspelo je"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Greška"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otvoreno je na telefonu"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Slika"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-be/strings.xml b/wear/wear/src/main/res/values-be/strings.xml
index 0fe9468..b9b7eaa 100644
--- a/wear/wear/src/main/res/values-be/strings.xml
+++ b/wear/wear/src/main/res/values-be/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Высоўнае меню навігацыі"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Высоўнае меню дзеянняў"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Выканана"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Памылка"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Адкрыта на тэлефоне"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Відарыс"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-bg/strings.xml b/wear/wear/src/main/res/values-bg/strings.xml
index 4330806..8f96402 100644
--- a/wear/wear/src/main/res/values-bg/strings.xml
+++ b/wear/wear/src/main/res/values-bg/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Слой за навигация"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Слой за действия"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Успешно"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Неуспешно"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Отворено на телефона ви"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Изображение"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-bn/strings.xml b/wear/wear/src/main/res/values-bn/strings.xml
index 66d6073..db4cb26 100644
--- a/wear/wear/src/main/res/values-bn/strings.xml
+++ b/wear/wear/src/main/res/values-bn/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"নেভিগেশন ড্রয়ার"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"অ্যাকশন ড্রয়ার"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"কাজটি সম্পূর্ণ হয়েছে"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"কাজটি করা যায়নি"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"আপনার ফোনে খোলা হয়েছে"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ছবি"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-bs/strings.xml b/wear/wear/src/main/res/values-bs/strings.xml
index aee1387..ee58387 100644
--- a/wear/wear/src/main/res/values-bs/strings.xml
+++ b/wear/wear/src/main/res/values-bs/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Ladica za navigaciju"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Ladica za radnju"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Uspješno"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Neuspješno"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otvoreno na telefonu"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Slika"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ca/strings.xml b/wear/wear/src/main/res/values-ca/strings.xml
index 38972ba..b490b00 100644
--- a/wear/wear/src/main/res/values-ca/strings.xml
+++ b/wear/wear/src/main/res/values-ca/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Tauler de navegació"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Tauler d\'accions"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Correcte"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Error"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"S\'ha obert al telèfon"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imatge"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-cs/strings.xml b/wear/wear/src/main/res/values-cs/strings.xml
index d37c363..a1bcbf6 100644
--- a/wear/wear/src/main/res/values-cs/strings.xml
+++ b/wear/wear/src/main/res/values-cs/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Vysouvací panel navigace"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Vysouvací panel akcí"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Úspěch"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Selhání"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otevřeno v telefonu"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Obrázek"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-da/strings.xml b/wear/wear/src/main/res/values-da/strings.xml
index 32691d8..fc4545d 100644
--- a/wear/wear/src/main/res/values-da/strings.xml
+++ b/wear/wear/src/main/res/values-da/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Sidemenu"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Handlingsmenu"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Gennemført"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Fejl"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Åbnet på din telefon"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Billede"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-de/strings.xml b/wear/wear/src/main/res/values-de/strings.xml
index 97734c7..11410bf 100644
--- a/wear/wear/src/main/res/values-de/strings.xml
+++ b/wear/wear/src/main/res/values-de/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigationsleiste"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Aktionsleiste"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Fertig"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Fehler"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Wurde auf dem Smartphone geöffnet"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Bild"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-el/strings.xml b/wear/wear/src/main/res/values-el/strings.xml
index 25cff0c..347fc44 100644
--- a/wear/wear/src/main/res/values-el/strings.xml
+++ b/wear/wear/src/main/res/values-el/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Συρτάρι πλοήγησης"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Συρτάρι ενεργειών"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Επιτυχία"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Αποτυχία"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Άνοιξε στο τηλέφωνό σας"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Εικόνα"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-en-rAU/strings.xml b/wear/wear/src/main/res/values-en-rAU/strings.xml
index 53d9019..136d9c9 100644
--- a/wear/wear/src/main/res/values-en-rAU/strings.xml
+++ b/wear/wear/src/main/res/values-en-rAU/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Success"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Failure"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opened on your phone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-en-rCA/strings.xml b/wear/wear/src/main/res/values-en-rCA/strings.xml
index 53d9019..136d9c9 100644
--- a/wear/wear/src/main/res/values-en-rCA/strings.xml
+++ b/wear/wear/src/main/res/values-en-rCA/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Success"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Failure"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opened on your phone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-en-rGB/strings.xml b/wear/wear/src/main/res/values-en-rGB/strings.xml
index 53d9019..136d9c9 100644
--- a/wear/wear/src/main/res/values-en-rGB/strings.xml
+++ b/wear/wear/src/main/res/values-en-rGB/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Success"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Failure"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opened on your phone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-en-rIN/strings.xml b/wear/wear/src/main/res/values-en-rIN/strings.xml
index 53d9019..136d9c9 100644
--- a/wear/wear/src/main/res/values-en-rIN/strings.xml
+++ b/wear/wear/src/main/res/values-en-rIN/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Success"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Failure"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opened on your phone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-en-rXC/strings.xml b/wear/wear/src/main/res/values-en-rXC/strings.xml
index e20077ec..3416c26 100644
--- a/wear/wear/src/main/res/values-en-rXC/strings.xml
+++ b/wear/wear/src/main/res/values-en-rXC/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Success"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Failure"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opened on your phone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-es-rUS/strings.xml b/wear/wear/src/main/res/values-es-rUS/strings.xml
index 9afe40c..7317e2e 100644
--- a/wear/wear/src/main/res/values-es-rUS/strings.xml
+++ b/wear/wear/src/main/res/values-es-rUS/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panel lateral de navegación"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panel lateral de acciones"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Listo"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Error"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Se abrió en el teléfono"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagen"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-es/strings.xml b/wear/wear/src/main/res/values-es/strings.xml
index ad29e18..38366d47b 100644
--- a/wear/wear/src/main/res/values-es/strings.xml
+++ b/wear/wear/src/main/res/values-es/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panel de navegación"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panel de acciones"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Correcto"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Error"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Abierto en el teléfono"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagen"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-et/strings.xml b/wear/wear/src/main/res/values-et/strings.xml
index 2dff6b1..26f85b4 100644
--- a/wear/wear/src/main/res/values-et/strings.xml
+++ b/wear/wear/src/main/res/values-et/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigeerimissahtel"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Toimingusahtel"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Õnnestus"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Ebaõnnestus"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Avatud teie telefonis"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Pilt"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-eu/strings.xml b/wear/wear/src/main/res/values-eu/strings.xml
index df7e326..91500a7 100644
--- a/wear/wear/src/main/res/values-eu/strings.xml
+++ b/wear/wear/src/main/res/values-eu/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Nabigazio-panel lerrakorra"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Ekintza-panel lerrakorra"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Gauzatu da ekintza"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Hutsegitea"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Telefonoan ireki da"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Irudia"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-fa/strings.xml b/wear/wear/src/main/res/values-fa/strings.xml
index d2f3e66..b7eeff2 100644
--- a/wear/wear/src/main/res/values-fa/strings.xml
+++ b/wear/wear/src/main/res/values-fa/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"کشوی پیمایش"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"کشوی فعالیت"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"موفق"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ناموفق"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"در تلفنتان باز شد"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"تصویر"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-fi/strings.xml b/wear/wear/src/main/res/values-fi/strings.xml
index 0216994..9de2f6b 100644
--- a/wear/wear/src/main/res/values-fi/strings.xml
+++ b/wear/wear/src/main/res/values-fi/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigoinnin vetopaneeli"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Toimintojen vetopaneeli"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Onnistui"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Epäonnistui"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Avattiin puhelimellasi"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Kuva"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-fr-rCA/strings.xml b/wear/wear/src/main/res/values-fr-rCA/strings.xml
index bb3a2ffb..f1a1db8 100644
--- a/wear/wear/src/main/res/values-fr-rCA/strings.xml
+++ b/wear/wear/src/main/res/values-fr-rCA/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panneau de navigation"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panneau d\'actions"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Réussite"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Échec"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Ouverte sur votre téléphone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-fr/strings.xml b/wear/wear/src/main/res/values-fr/strings.xml
index bea0b78..43c0f1b 100644
--- a/wear/wear/src/main/res/values-fr/strings.xml
+++ b/wear/wear/src/main/res/values-fr/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panneau de navigation"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panneau de commandes"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Opération réussie"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Échec"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Ouvert sur votre téléphone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Image"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-gl/strings.xml b/wear/wear/src/main/res/values-gl/strings.xml
index e6247d1..6e2faa6 100644
--- a/wear/wear/src/main/res/values-gl/strings.xml
+++ b/wear/wear/src/main/res/values-gl/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panel de navegación"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panel de accións"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Abriuse"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Produciuse un erro"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Abriuse no teléfono"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imaxe"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-gu/strings.xml b/wear/wear/src/main/res/values-gu/strings.xml
index 5fd6cf7..ecb9659 100644
--- a/wear/wear/src/main/res/values-gu/strings.xml
+++ b/wear/wear/src/main/res/values-gu/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"નૅવિગેશન ડ્રોઅર"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ઍક્શન ડ્રોઅર"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"સફળ રહ્યાં"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"નિષ્ફળ રહ્યાં"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"તમારા ફોન પર ખોલી"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"છબી"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-hi/strings.xml b/wear/wear/src/main/res/values-hi/strings.xml
index 1454fc1..fdb74dd 100644
--- a/wear/wear/src/main/res/values-hi/strings.xml
+++ b/wear/wear/src/main/res/values-hi/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"नेविगेशन पैनल"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"कार्रवाई पैनल"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"कार्रवाई सफल रही"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"कार्रवाई सफल नहीं हुई"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"आपके फ़ोन पर खोला गया"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"इमेज"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-hr/strings.xml b/wear/wear/src/main/res/values-hr/strings.xml
index aee1387..337fc59 100644
--- a/wear/wear/src/main/res/values-hr/strings.xml
+++ b/wear/wear/src/main/res/values-hr/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Ladica za navigaciju"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Ladica za radnju"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Uspjelo je"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Nije uspjelo"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otvoreno na telefonu"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Slika"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-hu/strings.xml b/wear/wear/src/main/res/values-hu/strings.xml
index f77c207..1aeab0e 100644
--- a/wear/wear/src/main/res/values-hu/strings.xml
+++ b/wear/wear/src/main/res/values-hu/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigációs fiók"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Műveleti fiók"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Sikerült"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Hiba"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Megnyitva a telefonján"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Kép"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-hy/strings.xml b/wear/wear/src/main/res/values-hy/strings.xml
index c5fa5df..11ae7b6 100644
--- a/wear/wear/src/main/res/values-hy/strings.xml
+++ b/wear/wear/src/main/res/values-hy/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Նավիգացիայի դարակ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Գործողությունների դարակ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Պատրաստ է"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Ձախողում"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Բացվել է ձեր հեռախոսում"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Պատկեր"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-in/strings.xml b/wear/wear/src/main/res/values-in/strings.xml
index 8c28e14..e122273 100644
--- a/wear/wear/src/main/res/values-in/strings.xml
+++ b/wear/wear/src/main/res/values-in/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panel navigasi"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panel samping tindakan"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Berhasil"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Gagal"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Dibuka di ponsel"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Gambar"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-is/strings.xml b/wear/wear/src/main/res/values-is/strings.xml
index 2fc00e4..44721f5 100644
--- a/wear/wear/src/main/res/values-is/strings.xml
+++ b/wear/wear/src/main/res/values-is/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Yfirlitsskúffa"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Aðgerðaskúffa"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Tókst"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Tókst ekki"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Opnað í símanum þínum"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Mynd"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-it/strings.xml b/wear/wear/src/main/res/values-it/strings.xml
index 4e9efb0..65e3d3b 100644
--- a/wear/wear/src/main/res/values-it/strings.xml
+++ b/wear/wear/src/main/res/values-it/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Riquadro di navigazione a scomparsa"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Riquadro a scomparsa delle azioni"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Operazione riuscita"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Errore"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Elemento aperto sul telefono"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Immagine"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-iw/strings.xml b/wear/wear/src/main/res/values-iw/strings.xml
index 6bcd69b..a07ab35 100644
--- a/wear/wear/src/main/res/values-iw/strings.xml
+++ b/wear/wear/src/main/res/values-iw/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"חלונית הזזה לניווט"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"חלונית הזזה לפעולות"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ההפעלה הצליחה"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"נכשלה"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"נפתחה בטלפון שלך"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"תמונה"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ja/strings.xml b/wear/wear/src/main/res/values-ja/strings.xml
index d5400eb..8fad2b8 100644
--- a/wear/wear/src/main/res/values-ja/strings.xml
+++ b/wear/wear/src/main/res/values-ja/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ナビゲーション ドロワー"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"アクション ドロワー"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"完了"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"エラー"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"スマートフォンで開きました"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"画像"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ka/strings.xml b/wear/wear/src/main/res/values-ka/strings.xml
index 9b1f566..6218c34 100644
--- a/wear/wear/src/main/res/values-ka/strings.xml
+++ b/wear/wear/src/main/res/values-ka/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ნავიგაციის უჯრა"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ქმედების უჯრა"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"წარმატება"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"შეფერხება"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"გახსნილია თქვენს ტელეფონში"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"სურათი"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-kk/strings.xml b/wear/wear/src/main/res/values-kk/strings.xml
index fe96962..9356103 100644
--- a/wear/wear/src/main/res/values-kk/strings.xml
+++ b/wear/wear/src/main/res/values-kk/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Навигация тартпасы"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Әрекеттер тартпасы"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Орындалды."</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Орындалмады."</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Телефонда ашылды."</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Сурет"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-km/strings.xml b/wear/wear/src/main/res/values-km/strings.xml
index 70abd5d..edead15 100644
--- a/wear/wear/src/main/res/values-km/strings.xml
+++ b/wear/wear/src/main/res/values-km/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ថតរុករក"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ថតសកម្មភាព"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ជោគជ័យ"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"បរាជ័យ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"បានបើកនៅលើទូរសព្ទរបស់អ្នក"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"រូបភាព"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-kn/strings.xml b/wear/wear/src/main/res/values-kn/strings.xml
index 0b9669c..1272aa1 100644
--- a/wear/wear/src/main/res/values-kn/strings.xml
+++ b/wear/wear/src/main/res/values-kn/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ನ್ಯಾವಿಗೇಶನ್ ಡ್ರಾಯರ್"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ಕ್ರಿಯೆ ಡ್ರಾಯರ್"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ಯಶಸ್ವಿಯಾಗಿದೆ"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ವೈಫಲ್ಯ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"ನಿಮ್ಮ ಫೋನ್ನಲ್ಲಿ ತೆರೆಯಲಾಗಿದೆ"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ಚಿತ್ರ"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ko/strings.xml b/wear/wear/src/main/res/values-ko/strings.xml
index 4f93d4a..f84d254 100644
--- a/wear/wear/src/main/res/values-ko/strings.xml
+++ b/wear/wear/src/main/res/values-ko/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"탐색 창"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"작업 창"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"완료"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"실패"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"휴대전화에서 열림"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"이미지"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ky/strings.xml b/wear/wear/src/main/res/values-ky/strings.xml
index 6324aa7..81ac8ea 100644
--- a/wear/wear/src/main/res/values-ky/strings.xml
+++ b/wear/wear/src/main/res/values-ky/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Чабыттоо суурмасы"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Аракет суурмасы"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Ийгилик"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Ката"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Саатыңызда ачылды"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Сүрөт"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-lo/strings.xml b/wear/wear/src/main/res/values-lo/strings.xml
index a405fa6..cdf495f 100644
--- a/wear/wear/src/main/res/values-lo/strings.xml
+++ b/wear/wear/src/main/res/values-lo/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ແຖບການນຳທາງ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ແຖບຄຳສັ່ງ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ສໍາເລັດແລ້ວ"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ບໍ່ສຳເລັດ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"ເປີດຢູ່ໂທລະສັບຂອງທ່ານແລ້ວ"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ຮູບ"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-lt/strings.xml b/wear/wear/src/main/res/values-lt/strings.xml
index 2ea598f..65b0019 100644
--- a/wear/wear/src/main/res/values-lt/strings.xml
+++ b/wear/wear/src/main/res/values-lt/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Naršymo skydelis"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Veiksmo skydelis"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Pavyko"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Nepavyko"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Atidaryta jūsų telefone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Vaizdas"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-lv/strings.xml b/wear/wear/src/main/res/values-lv/strings.xml
index b34c874..cfa6590 100644
--- a/wear/wear/src/main/res/values-lv/strings.xml
+++ b/wear/wear/src/main/res/values-lv/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigācijas atvilktne"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Darbību atvilktne"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Gatavs"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Kļūme"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Atvērts jūsu tālrunī"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Attēls"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-mk/strings.xml b/wear/wear/src/main/res/values-mk/strings.xml
index dbc5bac..0ed3a32 100644
--- a/wear/wear/src/main/res/values-mk/strings.xml
+++ b/wear/wear/src/main/res/values-mk/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Фиока за навигација"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Фиока за дејство"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Успешно"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Неуспешно"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Отворено на телефонот"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Слика"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ml/strings.xml b/wear/wear/src/main/res/values-ml/strings.xml
index f7347c0..8b8fc01 100644
--- a/wear/wear/src/main/res/values-ml/strings.xml
+++ b/wear/wear/src/main/res/values-ml/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"നാവിഗേഷൻ ഡ്രോയർ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ആക്ഷൻ ഡ്രോയർ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"വിജയകരം"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"പരാജയം"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"നിങ്ങളുടെ ഫോണിൽ തുറന്നത്"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ചിത്രം"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-mn/strings.xml b/wear/wear/src/main/res/values-mn/strings.xml
index 8728919..84b053f 100644
--- a/wear/wear/src/main/res/values-mn/strings.xml
+++ b/wear/wear/src/main/res/values-mn/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Навигацын шургуулга"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Үйлдлийн татуурга"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Амжилттай"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Амжилтгүй"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Таны утсан дээр нээсэн"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Зураг"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-mr/strings.xml b/wear/wear/src/main/res/values-mr/strings.xml
index 3a2e7a1..85b0637 100644
--- a/wear/wear/src/main/res/values-mr/strings.xml
+++ b/wear/wear/src/main/res/values-mr/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"नेव्हिगेशन ड्रॉवर"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"क्रिया ड्रॉवर"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"यशस्वीरीत्या पूर्ण केली"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"पूर्ण करता आली नाही"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"तुमच्या फोनवर उघडले"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"इमेज"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ms/strings.xml b/wear/wear/src/main/res/values-ms/strings.xml
index d64a574..4dc70c4 100644
--- a/wear/wear/src/main/res/values-ms/strings.xml
+++ b/wear/wear/src/main/res/values-ms/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Laci navigasi"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Laci tindakan"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Berjaya"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Gagal"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Dibuka pada telefon anda"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imej"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-my/strings.xml b/wear/wear/src/main/res/values-my/strings.xml
index ca60daf..54b4b34 100644
--- a/wear/wear/src/main/res/values-my/strings.xml
+++ b/wear/wear/src/main/res/values-my/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"လမ်းကြောင်းပြ အံဆွဲ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"လုပ်ဆောင်ချက် အံဆွဲ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"အောင်မြင်ပါသည်"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"မအောင်မြင်ပါ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"သင့်ဖုန်းတွင် ဖွင့်ထားသည်"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ပုံ"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-nb/strings.xml b/wear/wear/src/main/res/values-nb/strings.xml
index b02525d..f6deeb2 100644
--- a/wear/wear/src/main/res/values-nb/strings.xml
+++ b/wear/wear/src/main/res/values-nb/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Uttrekksmeny"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Uttrekksmeny for handlinger"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Fullført"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Mislyktes"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Åpnet på telefonen"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Bilde"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ne/strings.xml b/wear/wear/src/main/res/values-ne/strings.xml
index b8527a3..7a3783b 100644
--- a/wear/wear/src/main/res/values-ne/strings.xml
+++ b/wear/wear/src/main/res/values-ne/strings.xml
@@ -18,4 +18,12 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"नेभिगेसन ड्रअर"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"कारबाहीसम्बन्धी ड्रअर"</string>
+ <!-- no translation found for confirmation_overlay_a11y_description_success (2936753235333429892) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_description_fail (6412144553299494336) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_description_phone (8809042819843332546) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_type_image (4327971892987955014) -->
+ <skip />
</resources>
diff --git a/wear/wear/src/main/res/values-nl/strings.xml b/wear/wear/src/main/res/values-nl/strings.xml
index 760346d..da5ace8 100644
--- a/wear/wear/src/main/res/values-nl/strings.xml
+++ b/wear/wear/src/main/res/values-nl/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Zijmenu"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Actiemenu"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Gelukt"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Fout"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Geopend op je telefoon"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Afbeelding"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-or/strings.xml b/wear/wear/src/main/res/values-or/strings.xml
index a01f316..36176cb 100644
--- a/wear/wear/src/main/res/values-or/strings.xml
+++ b/wear/wear/src/main/res/values-or/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ନେଭିଗେଶନ୍ ପ୍ୟାନେଲ୍"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"କାର୍ଯ୍ୟକାରୀ ପ୍ୟାନେଲ୍"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ସଫଳ ହୋଇଛି"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ବିଫଳ ହୋଇଛି"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"ଆପଣଙ୍କ ଫୋନରେ ଖୋଲାଯାଇଛି"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ଛବି"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-pa/strings.xml b/wear/wear/src/main/res/values-pa/strings.xml
index f13d106..16298fa 100644
--- a/wear/wear/src/main/res/values-pa/strings.xml
+++ b/wear/wear/src/main/res/values-pa/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ਦਿਸ਼ਾ-ਨਿਰਦੇਸ਼ ਡ੍ਰਾਅਰ"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ਕਾਰਵਾਈ ਡ੍ਰਾਅਰ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"ਸਫਲਤਾ"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ਅਸਫਲਤਾ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"ਤੁਹਾਡੇ ਫ਼ੋਨ \'ਤੇ ਖੋਲ੍ਹਿਆ ਗਿਆ"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ਚਿੱਤਰ"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-pl/strings.xml b/wear/wear/src/main/res/values-pl/strings.xml
index fcb9b56..619928e 100644
--- a/wear/wear/src/main/res/values-pl/strings.xml
+++ b/wear/wear/src/main/res/values-pl/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panel nawigacji"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panel działań"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Udało się"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Błąd"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otwarto na telefonie"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Obraz"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-pt-rBR/strings.xml b/wear/wear/src/main/res/values-pt-rBR/strings.xml
index 46cf12e..59cf852 100644
--- a/wear/wear/src/main/res/values-pt-rBR/strings.xml
+++ b/wear/wear/src/main/res/values-pt-rBR/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Gaveta de navegação"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Gaveta de ações"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Pronto"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Falha"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Item aberto no seu smartphone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagem"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-pt-rPT/strings.xml b/wear/wear/src/main/res/values-pt-rPT/strings.xml
index 46cf12e..053696a 100644
--- a/wear/wear/src/main/res/values-pt-rPT/strings.xml
+++ b/wear/wear/src/main/res/values-pt-rPT/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Gaveta de navegação"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Gaveta de ações"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Êxito"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Falha"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Aberto no seu telemóvel"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagem"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-pt/strings.xml b/wear/wear/src/main/res/values-pt/strings.xml
index 46cf12e..59cf852 100644
--- a/wear/wear/src/main/res/values-pt/strings.xml
+++ b/wear/wear/src/main/res/values-pt/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Gaveta de navegação"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Gaveta de ações"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Pronto"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Falha"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Item aberto no seu smartphone"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagem"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ro/strings.xml b/wear/wear/src/main/res/values-ro/strings.xml
index 1cd2faf..a94e3ff 100644
--- a/wear/wear/src/main/res/values-ro/strings.xml
+++ b/wear/wear/src/main/res/values-ro/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Panou de navigare"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Panou de acțiune"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Succes"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Nereușită"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"S-a deschis pe telefon"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Imagine"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ru/strings.xml b/wear/wear/src/main/res/values-ru/strings.xml
index 226b67a..f2399d9 100644
--- a/wear/wear/src/main/res/values-ru/strings.xml
+++ b/wear/wear/src/main/res/values-ru/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Панель навигации"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Панель действий"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Готово"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Ошибка"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Открыто на телефоне"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Изображение"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-si/strings.xml b/wear/wear/src/main/res/values-si/strings.xml
index 2ce2823..cef6edd 100644
--- a/wear/wear/src/main/res/values-si/strings.xml
+++ b/wear/wear/src/main/res/values-si/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"සංචාලන ලාච්චුව"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ක්රියාමාර්ග ලාච්චුව"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"සාර්ථකයි"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"අසාර්ථකයි"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"ඔබගේ දුරකථනයෙහි විවෘත කරන ලදි"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"රූපය"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-sk/strings.xml b/wear/wear/src/main/res/values-sk/strings.xml
index aa39b5f..250e0e14 100644
--- a/wear/wear/src/main/res/values-sk/strings.xml
+++ b/wear/wear/src/main/res/values-sk/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigačný vysúvací panel"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Vysúvací panel akcií"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Úspech"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Nepodarilo sa"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Otvorené v telefóne"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Obrázok"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-sl/strings.xml b/wear/wear/src/main/res/values-sl/strings.xml
index 75ed0ee..9251c1c 100644
--- a/wear/wear/src/main/res/values-sl/strings.xml
+++ b/wear/wear/src/main/res/values-sl/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Predal za krmarjenje"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Predal z dejanji"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Uspelo je."</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Napaka"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Odprto v telefonu"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Slika"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-sq/strings.xml b/wear/wear/src/main/res/values-sq/strings.xml
index 3a0d059..494e982 100644
--- a/wear/wear/src/main/res/values-sq/strings.xml
+++ b/wear/wear/src/main/res/values-sq/strings.xml
@@ -18,4 +18,12 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Sirtari i navigimit"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Sirtari i veprimit"</string>
+ <!-- no translation found for confirmation_overlay_a11y_description_success (2936753235333429892) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_description_fail (6412144553299494336) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_description_phone (8809042819843332546) -->
+ <skip />
+ <!-- no translation found for confirmation_overlay_a11y_type_image (4327971892987955014) -->
+ <skip />
</resources>
diff --git a/wear/wear/src/main/res/values-sr/strings.xml b/wear/wear/src/main/res/values-sr/strings.xml
index 11cf001..b1e696e 100644
--- a/wear/wear/src/main/res/values-sr/strings.xml
+++ b/wear/wear/src/main/res/values-sr/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Фиока за навигацију"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Фиока за радњу"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Успело је"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Грешка"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Отворено је на телефону"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Слика"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-sv/strings.xml b/wear/wear/src/main/res/values-sv/strings.xml
index 4dddc5e..bd8dc5c 100644
--- a/wear/wear/src/main/res/values-sv/strings.xml
+++ b/wear/wear/src/main/res/values-sv/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigeringspanel"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Åtgärdspanel"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Klart"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Fel"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Öppnades på telefonen"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Bild"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-sw/strings.xml b/wear/wear/src/main/res/values-sw/strings.xml
index 4244cf4..df1df50 100644
--- a/wear/wear/src/main/res/values-sw/strings.xml
+++ b/wear/wear/src/main/res/values-sw/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Droo ya kusogeza"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Droo ya vitendo"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Imefaulu"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Imeshindwa"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Imefunguliwa kwenye simu yako"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Picha"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ta/strings.xml b/wear/wear/src/main/res/values-ta/strings.xml
index 4149d10..e696fe6 100644
--- a/wear/wear/src/main/res/values-ta/strings.xml
+++ b/wear/wear/src/main/res/values-ta/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"வழிசெலுத்தல் டிராயர்"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"செயல் டிராயர்"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"திறக்கப்பட்டது"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"திறக்க முடியவில்லை"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"உங்கள் மொபைலில் திறக்கப்பட்டது"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"படம்"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-te/strings.xml b/wear/wear/src/main/res/values-te/strings.xml
index c288710..16cb784 100644
--- a/wear/wear/src/main/res/values-te/strings.xml
+++ b/wear/wear/src/main/res/values-te/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"నావిగేషన్ డ్రాయర్"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"చర్య డ్రాయర్"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"విజయవంతమైంది"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"వైఫల్యం"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"మీ ఫోన్లో తెరవబడింది"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"ఇమేజ్"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-th/strings.xml b/wear/wear/src/main/res/values-th/strings.xml
index ea2a130..79fd276 100644
--- a/wear/wear/src/main/res/values-th/strings.xml
+++ b/wear/wear/src/main/res/values-th/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"ลิ้นชักการนำทาง"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"ลิ้นชักการดำเนินการ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"สำเร็จ"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ไม่สำเร็จ"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"เปิดในโทรศัพท์ของคุณแล้ว"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"รูปภาพ"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-tl/strings.xml b/wear/wear/src/main/res/values-tl/strings.xml
index 53d9019..62c0b9d 100644
--- a/wear/wear/src/main/res/values-tl/strings.xml
+++ b/wear/wear/src/main/res/values-tl/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigation drawer"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Action drawer"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Matagumpay"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Hindi Nagtagumpay"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Binuksan sa iyong telepono"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Larawan"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-tr/strings.xml b/wear/wear/src/main/res/values-tr/strings.xml
index ccfd319..b3747b1 100644
--- a/wear/wear/src/main/res/values-tr/strings.xml
+++ b/wear/wear/src/main/res/values-tr/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Gezinme çekmecesi"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"İşlem çekmecesi"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Başarılı"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Başarısız"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Telefonunuzda açıldı"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Resim"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-uk/strings.xml b/wear/wear/src/main/res/values-uk/strings.xml
index cfe9164..9d84935 100644
--- a/wear/wear/src/main/res/values-uk/strings.xml
+++ b/wear/wear/src/main/res/values-uk/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Панель навігації"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Висувна панель команд"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Готово"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Помилка"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Відкрито на вашому телефоні"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Зображення"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-ur/strings.xml b/wear/wear/src/main/res/values-ur/strings.xml
index 0756b77..c18ee62 100644
--- a/wear/wear/src/main/res/values-ur/strings.xml
+++ b/wear/wear/src/main/res/values-ur/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"نیویگیشن دراز"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"کارروائی دراز"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"کامیاب"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"ناکام"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"آپ کے فون پر کھول دی گئی"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"تصویر"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-uz/strings.xml b/wear/wear/src/main/res/values-uz/strings.xml
index bb4bf3b..633d3ef 100644
--- a/wear/wear/src/main/res/values-uz/strings.xml
+++ b/wear/wear/src/main/res/values-uz/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Navigatsiya paneli"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Buyruqlar paneli"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Tayyor"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Xatolik"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Telefoningizda ochildi"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Rasm"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-vi/strings.xml b/wear/wear/src/main/res/values-vi/strings.xml
index 6625a06a..c353e88 100644
--- a/wear/wear/src/main/res/values-vi/strings.xml
+++ b/wear/wear/src/main/res/values-vi/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Ngăn điều hướng"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Ngăn tác vụ"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Thành công"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Lỗi"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Đã mở trên điện thoại của bạn"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Hình ảnh"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-zh-rCN/strings.xml b/wear/wear/src/main/res/values-zh-rCN/strings.xml
index 3f532cd..e4622b2 100644
--- a/wear/wear/src/main/res/values-zh-rCN/strings.xml
+++ b/wear/wear/src/main/res/values-zh-rCN/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"抽屉式导航栏"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"操作抽屉式导航栏"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"成功"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"失败"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"已在您的手机上打开"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"图片"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-zh-rHK/strings.xml b/wear/wear/src/main/res/values-zh-rHK/strings.xml
index 0b64213..7a4679e 100644
--- a/wear/wear/src/main/res/values-zh-rHK/strings.xml
+++ b/wear/wear/src/main/res/values-zh-rHK/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"導覽列"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"操作導覽列"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"成功"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"失敗"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"已經喺手機度開咗"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"圖片"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-zh-rTW/strings.xml b/wear/wear/src/main/res/values-zh-rTW/strings.xml
index 4c0be1b..76a304e 100644
--- a/wear/wear/src/main/res/values-zh-rTW/strings.xml
+++ b/wear/wear/src/main/res/values-zh-rTW/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"導覽匣"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"動作導覽匣"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"成功"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"失敗"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"已在手機上開啟"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"圖片"</string>
</resources>
diff --git a/wear/wear/src/main/res/values-zu/strings.xml b/wear/wear/src/main/res/values-zu/strings.xml
index 33c6483..3cdb355 100644
--- a/wear/wear/src/main/res/values-zu/strings.xml
+++ b/wear/wear/src/main/res/values-zu/strings.xml
@@ -18,4 +18,8 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="ws_navigation_drawer_content_description" msgid="777117228299084604">"Ikhabethe lokuzulazula"</string>
<string name="ws_action_drawer_content_description" msgid="8863821639144537553">"Ikhabethe lesenzo"</string>
+ <string name="confirmation_overlay_a11y_description_success" msgid="2936753235333429892">"Impumelelo"</string>
+ <string name="confirmation_overlay_a11y_description_fail" msgid="6412144553299494336">"Ukwehluleka"</string>
+ <string name="confirmation_overlay_a11y_description_phone" msgid="8809042819843332546">"Ivuliwe kufoni yakho"</string>
+ <string name="confirmation_overlay_a11y_type_image" msgid="4327971892987955014">"Isithombe"</string>
</resources>
diff --git a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
index 0d97b77..d38b131 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/SidecarCompatDeviceTest.kt
@@ -19,9 +19,12 @@
package androidx.window.layout
import android.content.Context
+import android.content.pm.ActivityInfo
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.window.TestConfigChangeHandlingActivity
import androidx.window.WindowTestBase
import androidx.window.core.Version
import androidx.window.layout.ExtensionInterfaceCompat.ExtensionCallbackInterface
@@ -34,6 +37,9 @@
import com.nhaarman.mockitokotlin2.atLeastOnce
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertNotNull
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -47,6 +53,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
public class SidecarCompatDeviceTest : WindowTestBase(), CompatDeviceTestInterface {
private lateinit var sidecarCompat: SidecarCompat
@@ -73,6 +80,49 @@
}
}
+ @Test
+ fun testWindowLayoutCallbackOnConfigChange() {
+ val testScope = TestCoroutineScope()
+ testScope.runBlockingTest {
+ val scenario = ActivityScenario.launch(TestConfigChangeHandlingActivity::class.java)
+ val callbackInterface = mock<ExtensionCallbackInterface>()
+ scenario.onActivity { activity ->
+ val windowToken = getActivityWindowToken(activity)
+ assertNotNull(windowToken)
+ sidecarCompat.setExtensionCallback(callbackInterface)
+ sidecarCompat.onWindowLayoutChangeListenerAdded(activity)
+ activity.resetLayoutCounter()
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ activity.waitForLayout()
+ }
+ scenario.onActivity { activity ->
+ val windowToken = getActivityWindowToken(activity)
+ assertNotNull(windowToken)
+ val sidecarWindowLayoutInfo =
+ sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken)
+ verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(
+ any(),
+ argThat(SidecarMatcher(sidecarWindowLayoutInfo))
+ )
+ }
+ scenario.onActivity { activity ->
+ activity.resetLayoutCounter()
+ activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+ activity.waitForLayout()
+ }
+ scenario.onActivity { activity ->
+ val windowToken = getActivityWindowToken(activity)
+ assertNotNull(windowToken)
+ val updatedSidecarWindowLayoutInfo =
+ sidecarCompat.sidecar!!.getWindowLayoutInfo(windowToken)
+ verify(callbackInterface, atLeastOnce()).onWindowLayoutChanged(
+ any(),
+ argThat(SidecarMatcher(updatedSidecarWindowLayoutInfo))
+ )
+ }
+ }
+ }
+
private fun assumeExtensionV01() {
val sidecarVersion = SidecarCompat.sidecarVersion
assumeTrue(Version.VERSION_0_1 == sidecarVersion)
diff --git a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoRepositoryImplTest.kt b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoRepositoryImplTest.kt
index 734d865..d73c471 100644
--- a/window/window/src/androidTest/java/androidx/window/layout/WindowInfoRepositoryImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/layout/WindowInfoRepositoryImplTest.kt
@@ -29,7 +29,6 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.Executor
@@ -91,6 +90,7 @@
WindowMetricsCalculatorCompat,
FakeWindowBackend()
)
+ activity.resetLayoutCounter()
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
activity.waitForLayout()
testScope.launch {
@@ -98,6 +98,7 @@
}
}
scenario.onActivity { activity ->
+ activity.resetLayoutCounter()
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
activity.waitForLayout()
}
@@ -107,20 +108,6 @@
}
@Test
- public fun testGetMaximumWindowMetrics() {
- activityScenario.scenario.onActivity { testActivity ->
- val repo = WindowInfoRepositoryImpl(
- testActivity,
- WindowMetricsCalculatorCompat,
- FakeWindowBackend()
- )
- val expected = WindowMetricsCalculatorCompat.computeMaximumWindowMetrics(testActivity)
- val actual = repo.maximumWindowMetrics
- assertEquals(expected, actual)
- }
- }
-
- @Test
public fun testWindowLayoutFeatures(): Unit = testScope.runBlockingTest {
activityScenario.scenario.onActivity { testActivity ->
val windowMetricsCalculator = WindowMetricsCalculatorCompat
diff --git a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
index 6c97b75..4f60697 100644
--- a/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
+++ b/window/window/src/main/java/androidx/window/layout/SidecarCompat.kt
@@ -20,7 +20,9 @@
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.ComponentCallbacks
import android.content.Context
+import android.content.res.Configuration
import android.os.IBinder
import android.text.TextUtils
import android.util.Log
@@ -51,6 +53,9 @@
// Map of active listeners registered with #onWindowLayoutChangeListenerAdded() and not yet
// removed by #onWindowLayoutChangeListenerRemoved().
private val windowListenerRegisteredContexts = mutableMapOf<IBinder, Activity>()
+ // Map of activities registered to their component callbacks so we can keep track and
+ // remove when the activity is unregistered
+ private val componentCallbackMap = mutableMapOf<Activity, ComponentCallbacks>()
private var extensionCallback: ExtensionCallbackInterface? = null
constructor(context: Context) : this(
@@ -103,11 +108,36 @@
sidecar?.onDeviceStateListenersChanged(false)
}
extensionCallback?.onWindowLayoutChanged(activity, getWindowLayoutInfo(activity))
+ registerConfigurationChangeListener(activity)
+ }
+
+ private fun registerConfigurationChangeListener(activity: Activity) {
+ // Only register a component callback if we haven't already as register
+ // may be called multiple times for the same activity
+ if (componentCallbackMap[activity] == null) {
+ // Create a configuration change observer to send updated WindowLayoutInfo
+ // when the configuration of the app changes: b/186647126
+ val configChangeObserver = object : ComponentCallbacks {
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ extensionCallback?.onWindowLayoutChanged(
+ activity,
+ getWindowLayoutInfo(activity)
+ )
+ }
+
+ override fun onLowMemory() {
+ return
+ }
+ }
+ componentCallbackMap[activity] = configChangeObserver
+ activity.registerComponentCallbacks(configChangeObserver)
+ }
}
override fun onWindowLayoutChangeListenerRemoved(activity: Activity) {
val windowToken = getActivityWindowToken(activity) ?: return
sidecar?.onWindowLayoutChangeListenerRemoved(windowToken)
+ unregisterComponentCallback(activity)
val isLast = windowListenerRegisteredContexts.size == 1
windowListenerRegisteredContexts.remove(windowToken)
if (isLast) {
@@ -115,6 +145,12 @@
}
}
+ private fun unregisterComponentCallback(activity: Activity) {
+ val configChangeObserver = componentCallbackMap[activity]
+ activity.unregisterComponentCallbacks(configChangeObserver)
+ componentCallbackMap.remove(activity)
+ }
+
@SuppressLint("BanUncheckedReflection")
override fun validateExtensionInterface(): Boolean {
return try {
diff --git a/window/window/src/main/java/androidx/window/layout/WindowInfoRepositoryImpl.kt b/window/window/src/main/java/androidx/window/layout/WindowInfoRepositoryImpl.kt
index 44ace44..b351b5e 100644
--- a/window/window/src/main/java/androidx/window/layout/WindowInfoRepositoryImpl.kt
+++ b/window/window/src/main/java/androidx/window/layout/WindowInfoRepositoryImpl.kt
@@ -54,7 +54,6 @@
* current bounds that the user has selected for the [Activity][android.app.Activity]'s
* window.
*
- * @see maximumWindowMetrics
* @see android.view.WindowManager.getCurrentWindowMetrics
*/
override val currentWindowMetrics: Flow<WindowMetrics>
@@ -64,36 +63,6 @@
}
}
- /**
- * Returns the largest [WindowMetrics] an app may expect in the current system state.
- *
- *
- * The metrics describe the size of the largest potential area the window might occupy with
- * [MATCH_PARENT][android.view.WindowManager.LayoutParams.MATCH_PARENT] width and height
- * and any combination of flags that would allow the window to extend behind display cutouts.
- *
- *
- * The value of this is based on the largest **potential** windowing state of the system.
- * For example, for activities in multi-window mode the metrics returned are based on what the
- * bounds would be if the user expanded the window to cover the entire screen.
- *
- *
- * Note that this might still be smaller than the size of the physical display if certain
- * areas of the display are not available to windows created for the associated [Context].
- * For example, devices with foldable displays that wrap around the enclosure may split the
- * physical display into different regions, one for the front and one for the back, each acting
- * as different logical displays. In this case [.getMaximumWindowMetrics] would return
- * the region describing the side of the device the associated [context's][Context]
- * window is placed.
- *
- * @see currentWindowMetrics
- * @see android.view.WindowManager.getMaximumWindowMetrics
- */
- val maximumWindowMetrics: WindowMetrics
- get() {
- return windowMetricsCalculator.computeMaximumWindowMetrics(activity)
- }
-
private fun <T> configurationChanged(producer: () -> T): Flow<T> {
return flow {
val channel = Channel<T>(