Merge "Remove BenchmarkRule requirement to be used each test" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index e954274..5d0169f 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -40,6 +40,7 @@
androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
androidTestImplementation projectOrArtifact(":compose:material:material")
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(ANDROIDX_TEST_EXT_KTX)
androidTestImplementation(JUNIT)
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
index 6beff5a..734c7f2 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
@@ -16,14 +16,20 @@
package androidx.activity.compose
+import androidx.activity.OnBackPressedDispatcherOwner
+import androidx.activity.addCallback
import androidx.compose.material.Button
import androidx.compose.material.Text
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
@@ -100,4 +106,41 @@
assertThat(results).isEqualTo(listOf("initial", "changed"))
}
}
+
+ /**
+ * Test to ensure that the callback from the BackHandler remains in the correct order though
+ * lifecycle changes
+ */
+ @Test
+ fun testBackHandlerLifecycle() {
+ var inteceptedBack = false
+ val lifecycleOwner = TestLifecycleOwner()
+
+ composeTestRule.setContent {
+ val dispatcher = LocalOnBackPressedDispatcherOwner.current.onBackPressedDispatcher
+ val dispatcherOwner = object : OnBackPressedDispatcherOwner {
+ override fun getLifecycle() = lifecycleOwner.lifecycle
+
+ override fun getOnBackPressedDispatcher() = dispatcher
+ }
+ dispatcher.addCallback(lifecycleOwner) { }
+ CompositionLocalProvider(
+ LocalOnBackPressedDispatcherOwner provides dispatcherOwner,
+ LocalLifecycleOwner provides lifecycleOwner
+ ) {
+ BackHandler { inteceptedBack = true }
+ }
+ Button(onClick = { dispatcher.onBackPressed() }) {
+ Text(text = "Press Back")
+ }
+ }
+
+ lifecycleOwner.currentState = Lifecycle.State.CREATED
+ lifecycleOwner.currentState = Lifecycle.State.RESUMED
+
+ composeTestRule.onNodeWithText("Press Back").performClick()
+ composeTestRule.runOnIdle {
+ assertThat(inteceptedBack).isEqualTo(true)
+ }
+ }
}
diff --git a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
index 86f2967..0e890e7 100644
--- a/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
+++ b/activity/activity-compose/src/main/java/androidx/activity/compose/BackHandler.kt
@@ -28,6 +28,7 @@
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
/**
* Provides a [OnBackPressedDispatcher] that can be used by Composables hosted in a
@@ -86,10 +87,10 @@
backCallback.isEnabled = enabled
}
val backDispatcher = LocalOnBackPressedDispatcherOwner.current.onBackPressedDispatcher
- // If `backDispatcher` changes, dispose and reset the effect
- DisposableEffect(backDispatcher) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ DisposableEffect(lifecycleOwner, backDispatcher) {
// Add callback to the backDispatcher
- backDispatcher.addCallback(backCallback)
+ backDispatcher.addCallback(lifecycleOwner, backCallback)
// When the effect leaves the Composition, remove the callback
onDispose {
backCallback.remove()
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 279d455..33394d1 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -30,16 +30,16 @@
api("androidx.core:core-ktx:1.1.0") {
because "Mirror activity dependency graph for -ktx artifacts"
}
- api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")) {
+ api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
- api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"))
+ api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
api("androidx.savedstate:savedstate-ktx:1.1.0") {
because 'Mirror activity dependency graph for -ktx artifacts'
}
api(KOTLIN_STDLIB)
- androidTestImplementation(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-testing:2.3.1"))
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
androidTestImplementation(JUNIT)
androidTestImplementation(TRUTH)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt b/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
new file mode 100644
index 0000000..5f110c5
--- /dev/null
+++ b/activity/activity-ktx/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.activity.result
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.app.ActivityOptionsCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ActivityResultLauncherTest {
+
+ @Test
+ fun testUnitLaunch() {
+ val expectedResult = "result"
+ val registry = object : ActivityResultRegistry() {
+ override fun <I : Any?, O : Any?> onLaunch(
+ requestCode: Int,
+ contract: ActivityResultContract<I, O>,
+ input: I,
+ options: ActivityOptionsCompat?
+ ) {
+ contract.createIntent(InstrumentationRegistry.getInstrumentation().context, input)
+ dispatchResult(requestCode, expectedResult)
+ }
+ }
+
+ val contract = object : ActivityResultContract<Unit, String?>() {
+ override fun createIntent(context: Context, input: Unit) = Intent()
+ override fun parseResult(resultCode: Int, intent: Intent?) = ""
+ }
+
+ var actualResult: String? = null
+
+ val launcher = registry.register("key", contract) {
+ actualResult = it
+ }
+
+ launcher.launch()
+ assertThat(actualResult).isEqualTo(expectedResult)
+ }
+}
\ No newline at end of file
diff --git a/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt b/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
index 0b3d759..69ec900 100644
--- a/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
+++ b/activity/activity-ktx/src/main/java/androidx/activity/result/ActivityResultLauncher.kt
@@ -30,5 +30,5 @@
*/
@JvmName("launchUnit")
public fun ActivityResultLauncher<Unit>.launch(options: ActivityOptionsCompat? = null) {
- launch(null, options)
+ launch(Unit, options)
}
\ No newline at end of file
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 9be5f88..3f497dc 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -1,8 +1,8 @@
-import static androidx.build.dependencies.DependenciesKt.*
import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -23,13 +23,13 @@
api("androidx.annotation:annotation:1.1.0")
implementation("androidx.collection:collection:1.0.0")
api("androidx.core:core:1.1.0")
- api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime:2.3.1"))
- api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel:2.3.1"))
+ api("androidx.lifecycle:lifecycle-runtime:2.3.1")
+ api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
api("androidx.savedstate:savedstate:1.1.0")
- api(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1"))
+ api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
implementation("androidx.tracing:tracing:1.0.0")
- androidTestImplementation(prebuiltOrSnapshot("androidx.lifecycle:lifecycle-runtime-testing:2.3.1"))
+ androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
androidTestImplementation(KOTLIN_STDLIB)
androidTestImplementation(LEAKCANARY)
androidTestImplementation(LEAKCANARY_INSTRUMENTATION)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt
new file mode 100644
index 0000000..8a3b2aa
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchUtils.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.isSupportedWithVmSettings
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+
+const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
+
+fun MacrobenchmarkRule.measureStartup(
+ compilationMode: CompilationMode,
+ startupMode: StartupMode,
+ iterations: Int = 3,
+ setupIntent: Intent.() -> Unit = {}
+) = measureRepeated(
+ packageName = TARGET_PACKAGE,
+ metrics = listOf(StartupTimingMetric()),
+ compilationMode = compilationMode,
+ iterations = iterations,
+ startupMode = startupMode
+) {
+ pressHome()
+ val intent = Intent()
+ intent.setPackage(TARGET_PACKAGE)
+ setupIntent(intent)
+ startActivityAndWait(intent)
+}
+
+fun createStartupCompilationParams(
+ startupModes: List<StartupMode> = listOf(StartupMode.HOT, StartupMode.WARM, StartupMode.COLD),
+ compilationModes: List<CompilationMode> = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile()
+ )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+ for (startupMode in startupModes) {
+ for (compilationMode in compilationModes) {
+ // Skip configs that can't run, so they don't clutter Studio benchmark
+ // output with AssumptionViolatedException dumps
+ if (compilationMode.isSupportedWithVmSettings()) {
+ add(arrayOf(startupMode, compilationMode))
+ }
+ }
+ }
+}
+
+fun createCompilationParams(
+ compilationModes: List<CompilationMode> = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile()
+ )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+ for (compilationMode in compilationModes) {
+ // Skip configs that can't run, so they don't clutter Studio benchmark
+ // output with AssumptionViolatedException dumps
+ if (compilationMode.isSupportedWithVmSettings()) {
+ add(arrayOf(compilationMode))
+ }
+ }
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
deleted file mode 100644
index d4be8d52..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class)
-class ProcessSpeedProfileValidation(
- private val compilationMode: CompilationMode,
- private val startupMode: StartupMode
-) {
- @get:Rule
- val benchmarkRule = MacrobenchmarkRule()
-
- @Test
- fun start() = benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = listOf(StartupTimingMetric()),
- compilationMode = compilationMode,
- iterations = 3,
- startupMode = startupMode
- ) {
- pressHome()
- startActivityAndWait()
- }
-
- companion object {
- private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
-
- @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
- @JvmStatic
- fun kilProcessParameters(): List<Array<Any>> {
- val compilationModes = listOf(
- CompilationMode.None,
- CompilationMode.SpeedProfile(warmupIterations = 3)
- )
- val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
- return compilationModes.zip(processKillOptions).map {
- arrayOf(it.first, it.second)
- }
- }
- }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 742d7dc..553c577 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.integration.macrobenchmark
+import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -26,13 +27,16 @@
@LargeTest
@RunWith(Parameterized::class)
-class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+class SmallListStartupBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureStartup(
- profileCompiled = true,
+ compilationMode = compilationMode,
startupMode = startupMode
) {
action = "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
@@ -40,11 +44,8 @@
}
companion object {
- @Parameterized.Parameters(name = "mode={0}")
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters(): List<Array<Any>> {
- return listOf(StartupMode.COLD, StartupMode.WARM)
- .map { arrayOf(it) }
- }
+ fun parameters() = createStartupCompilationParams()
}
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
deleted file mode 100644
index f07501f..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.integration.macrobenchmark
-
-import android.content.Intent
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-
-const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
-
-fun MacrobenchmarkRule.measureStartup(
- profileCompiled: Boolean,
- startupMode: StartupMode,
- iterations: Int = 3,
- setupIntent: Intent.() -> Unit = {}
-) = measureRepeated(
- packageName = "androidx.benchmark.integration.macrobenchmark.target",
- metrics = listOf(StartupTimingMetric()),
- compilationMode = if (profileCompiled) {
- CompilationMode.SpeedProfile(warmupIterations = 3)
- } else {
- CompilationMode.None
- },
- iterations = iterations,
- startupMode = startupMode
-) {
- pressHome()
- val intent = Intent()
- intent.setPackage(TARGET_PACKAGE)
- setupIntent(intent)
- startActivityAndWait(intent)
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 89%
rename from benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
rename to benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
index b9ce5d4..62ede92 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/FrameTimingMetricValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBenchmark.kt
@@ -35,7 +35,7 @@
@LargeTest
@SdkSuppress(minSdkVersion = 29)
@RunWith(Parameterized::class)
-class FrameTimingMetricValidation(
+class TrivialListScrollBenchmark(
private val compilationMode: CompilationMode
) {
@get:Rule
@@ -79,13 +79,8 @@
"androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
private const val RESOURCE_ID = "recycler"
- @Parameterized.Parameters(name = "compilation_mode={0}")
+ @Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun jankParameters(): List<Array<Any>> {
- return listOf(
- CompilationMode.None,
- CompilationMode.SpeedProfile(warmupIterations = 3)
- ).map { arrayOf(it) }
- }
+ fun parameters() = createCompilationParams()
}
}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
index e62457a..29d9be3 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -16,6 +16,7 @@
package androidx.benchmark.integration.macrobenchmark
+import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -26,24 +27,24 @@
@LargeTest
@RunWith(Parameterized::class)
-class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+class TrivialStartupBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureStartup(
- profileCompiled = true,
+ compilationMode = compilationMode,
startupMode = startupMode
) {
action = "androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
}
companion object {
- @Parameterized.Parameters(name = "mode={0}")
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters(): List<Array<Any>> {
- return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
- .map { arrayOf(it) }
- }
+ fun parameters() = createStartupCompilationParams()
}
}
diff --git a/benchmark/macro/build.gradle b/benchmark/macro/build.gradle
index 72c0ac1..1c61043 100644
--- a/benchmark/macro/build.gradle
+++ b/benchmark/macro/build.gradle
@@ -47,11 +47,11 @@
implementation(project(":benchmark:benchmark-common"))
implementation(ANDROIDX_TEST_CORE)
implementation(ANDROIDX_TEST_UIAUTOMATOR)
- implementation(ANDROIDX_TEST_RULES)
androidTestImplementation(project(":internal-testutils-ktx"))
androidTestImplementation(project(":tracing:tracing-ktx"))
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
androidTestImplementation(ANDROIDX_TEST_RUNNER)
androidTestImplementation(KOTLIN_TEST)
}
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
new file mode 100644
index 0000000..11ae6ab
--- /dev/null
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CompilationModeTest {
+ private val vmRunningInterpretedOnly: Boolean
+
+ init {
+ val device = InstrumentationRegistry.getInstrumentation().device()
+ val getProp = device.executeShellCommand("getprop dalvik.vm.extra-opts")
+ vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
+ }
+
+ @Test
+ fun names() {
+ // We test these names, as they're likely built into parameterized
+ // test strings, so stability/brevity are important
+ assertEquals("None", CompilationMode.None.toString())
+ assertEquals("SpeedProfile(iterations=123)", CompilationMode.SpeedProfile(123).toString())
+ assertEquals("Speed", CompilationMode.Speed.toString())
+ assertEquals("Interpreted", CompilationMode.Interpreted.toString())
+ }
+
+ @Test
+ fun isSupportedWithVmSettings_jitEnabled() {
+ assumeFalse(vmRunningInterpretedOnly)
+
+ assertTrue(CompilationMode.None.isSupportedWithVmSettings())
+ assertTrue(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
+ assertTrue(CompilationMode.Speed.isSupportedWithVmSettings())
+ assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
+ }
+
+ @Test
+ fun isSupportedWithVmSettings_jitDisabled() {
+ assumeTrue(vmRunningInterpretedOnly)
+
+ assertFalse(CompilationMode.None.isSupportedWithVmSettings())
+ assertFalse(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
+ assertFalse(CompilationMode.Speed.isSupportedWithVmSettings())
+ assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
+ }
+}
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 30529d6..b52e20e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -14,23 +14,10 @@
* limitations under the License.
*/
-package androidx.benchmark.macro/*
- * 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.test.platform.app.InstrumentationRegistry
+import org.junit.AssumptionViolatedException
sealed class CompilationMode(
// for modes other than [None], is argument passed `cmd package compile`
@@ -44,16 +31,19 @@
}
object None : CompilationMode(null) {
- override fun toString() = "androidx.benchmark.macro.CompilationMode.None"
+ override fun toString() = "None"
}
class SpeedProfile(val warmupIterations: Int = 3) : CompilationMode("speed-profile") {
- override fun toString() =
- "androidx.benchmark.macro.CompilationMode.SpeedProfile(iterations=$warmupIterations)"
+ override fun toString() = "SpeedProfile(iterations=$warmupIterations)"
}
object Speed : CompilationMode("speed") {
- override fun toString() = "androidx.benchmark.macro.CompilationMode.Speed"
+ override fun toString() = "Speed"
+ }
+
+ object Interpreted : CompilationMode(null) {
+ override fun toString() = "Interpreted"
}
}
@@ -61,7 +51,7 @@
val instrumentation = InstrumentationRegistry.getInstrumentation()
// Clear profile between runs.
clearProfile(instrumentation, packageName)
- if (this == CompilationMode.None) {
+ if (this == CompilationMode.None || this == CompilationMode.Interpreted) {
return // nothing to do
}
if (this is CompilationMode.SpeedProfile) {
@@ -75,4 +65,38 @@
packageName,
compileArgument()
)
+}
+
+fun CompilationMode.isSupportedWithVmSettings(): Boolean {
+ val device = InstrumentationRegistry.getInstrumentation().device()
+ val getProp = device.executeShellCommand("getprop dalvik.vm.extra-opts")
+ val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
+
+ // true if requires interpreted, false otherwise
+ val interpreted = this == CompilationMode.Interpreted
+ return vmRunningInterpretedOnly == interpreted
+}
+
+internal fun CompilationMode.assumeSupportedWithVmSettings() {
+ if (!isSupportedWithVmSettings()) {
+ throw AssumptionViolatedException(
+ when {
+ DeviceInfo.isRooted && this == CompilationMode.Interpreted ->
+ """
+ To run benchmarks with CompilationMode $this,
+ you must disable jit on your device with the following command:
+ `adb shell setprop dalvik.vm.extra-opts -Xusejit:false; adb shell stop; adb shell start`
+ """.trimIndent()
+ DeviceInfo.isRooted && this != CompilationMode.Interpreted ->
+ """
+ To run benchmarks with CompilationMode $this,
+ you must enable jit on your device with the following command:
+ `adb shell setprop dalvik.vm.extra-opts \"\"; adb shell stop; adb shell start`
+ """.trimIndent()
+ else ->
+ "You must toggle usejit on the VM to use CompilationMode $this, this requires" +
+ "rooting your device."
+ }
+ )
+ }
}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
index 062ad0f..a93a8b7 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/DeviceInfo.kt
@@ -21,6 +21,7 @@
import android.os.BatteryManager
import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
internal object DeviceInfo {
val isEmulator = Build.FINGERPRINT.startsWith("generic") ||
@@ -34,6 +35,20 @@
val isEngBuild = Build.FINGERPRINT.contains(":eng/")
+ val isRooted =
+ arrayOf(
+ "/system/app/Superuser.apk",
+ "/sbin/su",
+ "/system/bin/su",
+ "/system/xbin/su",
+ "/data/local/xbin/su",
+ "/data/local/bin/su",
+ "/system/sd/xbin/su",
+ "/system/bin/failsafe/su",
+ "/data/local/su",
+ "/su/bin/su"
+ ).any { File(it).exists() }
+
/**
* Battery percentage required to avoid low battery warning.
*
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 021bcee..f2b1952 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -111,6 +111,9 @@
"Macrobenchmark currently requires Android 10 (API 29) or greater."
}
+ // skip benchmark if not supported by vm settings
+ compilationMode.assumeSupportedWithVmSettings()
+
val suppressionState = checkErrors(packageName)
var warningMessage = suppressionState?.warningMessage ?: ""
diff --git a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
index a7fc625..a1439a7 100644
--- a/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
+++ b/browser/browser/src/androidTest/java/androidx/browser/trusted/TrustedWebActivityServiceConnectionPoolTest.java
@@ -36,6 +36,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -78,6 +79,7 @@
mManager.unbindAllConnections();
}
+ @Ignore("Test disabled due to flakiness, see b/182415874")
@Test
public void testConnection() {
final AtomicBoolean connected = new AtomicBoolean();
diff --git a/buildSrc/OWNERS b/buildSrc/OWNERS
index f852f60..7674fe0 100644
--- a/buildSrc/OWNERS
+++ b/buildSrc/OWNERS
@@ -9,5 +9,6 @@
per-file *Dependencies.kt=file://OWNERS
per-file *LibraryVersions.kt=file://OWNERS
per-file *PublishDocsRules.kt=file://OWNERS
+per-file *build_dependencies.gradle=file://OWNERS
per-file *AndroidXPlaygroundRootPlugin.kt = [email protected], [email protected], [email protected]
diff --git a/buildSrc/build_dependencies.gradle b/buildSrc/build_dependencies.gradle
index 5065107..935497b 100644
--- a/buildSrc/build_dependencies.gradle
+++ b/buildSrc/build_dependencies.gradle
@@ -23,7 +23,7 @@
build_versions.lint = build_versions.studio["lint"]
build_versions.kotlin = "1.4.31"
-build_versions.kotlin_coroutines = "1.4.1"
+build_versions.kotlin_coroutines = "1.4.3"
build_versions.ksp = "1.4.30-1.0.0-alpha05"
build_versions.hilt = "2.33-beta"
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
index 365c7c9..ebf8a0b 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlaygroundRootPlugin.kt
@@ -16,7 +16,6 @@
package androidx.build
-import androidx.build.AndroidXRootPlugin.Companion.PREBUILT_OR_SNAPSHOT_EXT_NAME
import androidx.build.AndroidXRootPlugin.Companion.PROJECT_OR_ARTIFACT_EXT_NAME
import androidx.build.gradle.isRoot
import groovy.xml.DOMBuilder
@@ -55,12 +54,6 @@
}
)
- private val prebuiltOrSnapshotClosure = KotlinClosure1<String, String>(
- function = {
- prebuiltOrSnapshot(this)
- }
- )
-
override fun apply(target: Project) {
if (!target.isRoot) {
throw GradleException("This plugin should only be applied to root project")
@@ -82,7 +75,6 @@
private fun configureSubProject(project: Project) {
project.repositories.addPlaygroundRepositories()
project.extra.set(PROJECT_OR_ARTIFACT_EXT_NAME, projectOrArtifactClosure)
- project.extra.set(PREBUILT_OR_SNAPSHOT_EXT_NAME, prebuiltOrSnapshotClosure)
project.configurations.all { configuration ->
configuration.resolutionStrategy.dependencySubstitution.all { substitution ->
substitution.replaceIfSnapshot()
@@ -127,21 +119,6 @@
}
}
- private fun prebuiltOrSnapshot(path: String): String {
- val sections = path.split(":")
-
- if (sections.size != 3) {
- throw GradleException(
- "Expected prebuiltOrSnapshot path to be of the form " +
- "<group>:<artifact>:<version>, but was $path"
- )
- }
-
- val group = sections[0]
- val artifact = sections[1]
- return "$group:$artifact:$SNAPSHOT_MARKER"
- }
-
private fun DependencySubstitution.replaceIfSnapshot() {
val requested = this.requested
if (requested is ModuleComponentSelector && requested.version == SNAPSHOT_MARKER) {
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
index ce0b8d1..471adb5 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXRootPlugin.kt
@@ -101,15 +101,6 @@
}
)
)
- project.extra.set(
- PREBUILT_OR_SNAPSHOT_EXT_NAME,
- KotlinClosure1<String, String>(
- function = {
- // this refers to the first parameter of the closure.
- this
- }
- )
- )
project.plugins.withType(AndroidBasePlugin::class.java) {
buildOnServerTask.dependsOn("${project.path}:assembleDebug")
buildOnServerTask.dependsOn("${project.path}:assembleAndroidTest")
@@ -245,6 +236,5 @@
companion object {
const val PROJECT_OR_ARTIFACT_EXT_NAME = "projectOrArtifact"
- const val PREBUILT_OR_SNAPSHOT_EXT_NAME = "prebuiltOrSnapshot"
}
}
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index a099596..eee159c 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -74,14 +74,14 @@
val INSPECTION = Version("1.0.0")
val INTERPOLATOR = Version("1.1.0-alpha01")
val JETIFIER = Version("1.0.0-beta10")
- val LEANBACK = Version("1.1.0-beta01")
+ val LEANBACK = Version("1.2.0-alpha01")
val LEANBACK_PAGING = Version("1.1.0-beta01")
- val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
+ val LEANBACK_PREFERENCE = Version("1.2.0-alpha01")
val LEANBACK_TAB = Version("1.1.0-beta01")
val LEGACY = Version("1.1.0-alpha01")
val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
val LIFECYCLE = Version("2.4.0-alpha01")
- val LIFECYCLE_COMPOSE = Version("1.0.0-alpha04")
+ val LIFECYCLE_VIEWMODEL_COMPOSE = Version("1.0.0-alpha04")
val LIFECYCLE_EXTENSIONS = Version("2.2.0")
val LOADER = Version("1.2.0-alpha01")
val MEDIA = Version("1.3.0-rc01")
@@ -89,7 +89,7 @@
val MEDIAROUTER = Version("1.3.0-alpha01")
val NAVIGATION = Version("2.4.0-alpha01")
val NAVIGATION_COMPOSE = Version("1.0.0-alpha10")
- val PAGING = Version("3.0.0-beta03")
+ val PAGING = Version("3.1.0-alpha01")
val PAGING_COMPOSE = Version("1.0.0-alpha08")
val PALETTE = Version("1.1.0-alpha01")
val PRINT = Version("1.1.0-beta01")
@@ -112,7 +112,7 @@
val SLICE_BUILDERS_KTX = Version("1.0.0-alpha08")
val SLICE_REMOTECALLBACK = Version("1.0.0-alpha01")
val SLIDINGPANELAYOUT = Version("1.2.0-alpha01")
- val STARTUP = Version("1.0.0")
+ val STARTUP = Version("1.1.0-alpha01")
val SQLITE = Version("2.2.0-alpha01")
val SQLITE_INSPECTOR = Version("2.1.0-alpha01")
val SWIPEREFRESHLAYOUT = Version("1.2.0-alpha01")
@@ -128,20 +128,25 @@
val VERSIONED_PARCELABLE = Version("1.2.0-alpha01")
val VIEWPAGER = Version("1.1.0-alpha01")
val VIEWPAGER2 = Version("1.1.0-alpha02")
- val WEAR = Version("1.2.0-alpha07")
- val WEAR_COMPLICATIONS = Version("1.0.0-alpha10")
+ val WEAR = Version("1.2.0-alpha08")
+ val WEAR_COMPLICATIONS_DATA = Version("1.0.0-alpha11")
+ val WEAR_COMPLICATIONS_PROVIDER = Version("1.0.0-alpha11")
val WEAR_INPUT = Version("1.1.0-alpha02")
+ val WEAR_INPUT_TESTING = WEAR_INPUT
val WEAR_ONGOING = Version("1.0.0-alpha04")
val WEAR_PHONE_INTERACTIONS = Version("1.0.0-alpha04")
val WEAR_REMOTE_INTERACTIONS = Version("1.0.0-alpha03")
val WEAR_TILES = Version("1.0.0-alpha02")
- val WEAR_WATCHFACE = Version("1.0.0-alpha10")
- val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha10")
- val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha10")
- val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha10")
- val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha10")
+ val WEAR_WATCHFACE = Version("1.0.0-alpha11")
+ val WEAR_WATCHFACE_CLIENT = Version("1.0.0-alpha11")
+ val WEAR_WATCHFACE_CLIENT_GUAVA = WEAR_WATCHFACE_CLIENT
+ val WEAR_WATCHFACE_COMPLICATIONS_RENDERING = Version("1.0.0-alpha11")
+ val WEAR_WATCHFACE_DATA = Version("1.0.0-alpha11")
+ val WEAR_WATCHFACE_EDITOR = Version("1.0.0-alpha11")
+ val WEAR_WATCHFACE_EDITOR_GUAVA = WEAR_WATCHFACE_EDITOR
+ val WEAR_WATCHFACE_STYLE = Version("1.0.0-alpha11")
val WEBKIT = Version("1.5.0-alpha01")
- val WINDOW = Version("1.0.0-alpha05")
+ val WINDOW = Version("1.0.0-alpha06")
val WINDOW_EXTENSIONS = Version("1.0.0-alpha01")
val WINDOW_SIDECAR = Version("0.1.0-alpha01")
val WORK = Version("2.6.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index dda6d40..0d5b532 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -27,7 +27,7 @@
const val ANDROIDX_TEST_RULES = "androidx.test:rules:1.3.0"
const val ANDROIDX_TEST_RUNNER = "androidx.test:runner:1.3.0"
const val ANDROIDX_TEST_UIAUTOMATOR = "androidx.test.uiautomator:uiautomator:2.2.0"
-const val AUTO_COMMON = "com.google.auto:auto-common:0.10"
+const val AUTO_COMMON = "com.google.auto:auto-common:0.11"
const val AUTO_SERVICE_ANNOTATIONS = "com.google.auto.service:auto-service-annotations:1.0-rc6"
const val AUTO_SERVICE_PROCESSOR = "com.google.auto.service:auto-service:1.0-rc6"
const val AUTO_VALUE = "com.google.auto.value:auto-value:1.6.3"
@@ -108,7 +108,7 @@
const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.18"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.2.21"
val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
@@ -127,6 +127,7 @@
}
}
const val TRUTH = "com.google.truth:truth:1.0.1"
+const val VIEW_BINDING = "androidx.databinding:viewbinding:4.1.2"
const val XERIAL = "org.xerial:sqlite-jdbc:3.25.2"
const val XPP3 = "xpp3:xpp3:1.1.4c"
const val XMLPULL = "xmlpull:xmlpull:1.1.3.1"
diff --git a/busytown/androidx-studio-integration.sh b/busytown/androidx-studio-integration.sh
index 22d6b80..1bb624c 100755
--- a/busytown/androidx-studio-integration.sh
+++ b/busytown/androidx-studio-integration.sh
@@ -59,11 +59,11 @@
zip -r "$DIST_DIR/transforms.zip" "$OUT_DIR/.gradle/caches/transforms-2/files-2.1"
}
-if buildAndroidx; then
- echo build succeeded
-else
- # b/162260809 export transforms directory to help identify cause of corrupt/missing files
- exportTransformsDir
- exit 1
-fi
+#if buildAndroidx; then
+# echo build succeeded
+#else
+# # b/162260809 export transforms directory to help identify cause of corrupt/missing files
+# exportTransformsDir
+# exit 1
+#fi
echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
index 02ad9bb..e10908c 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -52,7 +52,7 @@
* well as providing access to other utility methods. The primary purpose of this class it to
* forward these interactions to the currently configured [UseCaseCamera].
*/
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
@CameraScope
@OptIn(ExperimentalCoroutinesApi::class)
class CameraControlAdapter @Inject constructor(
@@ -157,7 +157,7 @@
warn { "TODO: cancelAfAeTrigger is not yet supported" }
}
- @SuppressLint("UnsafeExperimentalUsageError")
+ @SuppressLint("UnsafeOptInUsageError")
override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> {
return threads.scope.async(start = CoroutineStart.UNDISPATCHED) {
useCaseManager.camera?.let {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 311be3f..01a351e 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -43,7 +43,7 @@
* Adapt the [CameraInfoInternal] interface to [CameraPipe].
*/
@SuppressLint(
- "UnsafeExperimentalUsageError" // Suppressed due to experimental ExposureState
+ "UnsafeOptInUsageError" // Suppressed due to experimental ExposureState
)
@CameraScope
class CameraInfoAdapter @Inject constructor(
@@ -82,7 +82,7 @@
override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomStateLiveData
override fun getTorchState(): LiveData<Int> = cameraState.torchStateLiveData
- @SuppressLint("UnsafeExperimentalUsageError")
+ @SuppressLint("UnsafeOptInUsageError")
override fun getExposureState(): ExposureState = cameraState.exposureStateLiveData.value!!
override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
index 22ed619..3ddc4ff0 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraStateAdapter.kt
@@ -31,7 +31,7 @@
/**
* [CameraStateAdapter] caches and updates based on callbacks from the active CameraGraph.
*/
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
@CameraScope
class CameraStateAdapter @Inject constructor(
private val zoomControl: ZoomControl,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
index 2ea0afa..2837159 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/EvCompValue.kt
@@ -24,7 +24,7 @@
/**
* Immutable adaptor to the ExposureState interface.
*/
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
data class EvCompValue(
private val supported: Boolean,
private val index: Int,
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
index 98b6e81..487b34b 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -27,7 +27,7 @@
internal val EMPTY_RANGE = Range(0, 0)
/** Adapt [ExposureState] to a [CameraMetadata] instance. */
-@SuppressLint("UnsafeExperimentalUsageError")
+@SuppressLint("UnsafeOptInUsageError")
class ExposureStateAdapter(
private val cameraProperties: CameraProperties,
private val exposureCompensation: Int
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
index 7f66dc9..168ca3146 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraControls.kt
@@ -17,6 +17,8 @@
package androidx.camera.camera2.pipe
import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
// Public controls and enums used to interact with a CameraGraph.
@@ -130,11 +132,14 @@
/**
* Return type for a 3A method.
*
- * @param frameNumber the latest [FrameNumber] at which the method succeeded or was aborted.
* @param status [Status] of the 3A operation at the time of return.
+ * @param frameMetadata [FrameMetadata] of the latest frame at which the method succeeded or was
+ * aborted. The metadata reflects CaptureResult or TotalCaptureResult for that frame. It can so
+ * happen that the [CaptureResult] itself has all the key-value pairs needed to determine the
+ * completion of the method, in that case this frameMetadata may not contain all the kay value pairs
+ * associated with the final result i.e [TotalCaptureResult] of this frame.
*/
-public data class Result3A(val frameNumber: FrameNumber, val status: Status) {
-
+public data class Result3A(val status: Status, val frameMetadata: FrameMetadata? = null) {
/**
* Enum to know the status of 3A operation in case the method returns before the desired
* operation is complete. The reason could be that the operation was talking a lot longer and an
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 2154f40..2c09227 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -24,15 +24,12 @@
import android.hardware.camera2.params.MeteringRectangle
import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_MS
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
import kotlinx.coroutines.Deferred
import java.io.Closeable
/**
* A [CameraGraph] represents the combined configuration and state of a camera.
- *
- *
*/
public interface CameraGraph : Closeable {
public val streams: StreamGraph
@@ -237,7 +234,15 @@
/**
* Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
- * behaviors.
+ * behaviors. This given 3A parameters are applied before the lock is obtained. If 'null'
+ * value is passed for a parameter, that parameter is ignored, and the current value for
+ * that parameter continues to be applied.
+ *
+ * TODO(sushilnath@): Add support for specifying the AE, AF and AWB modes as well. The
+ * update of modes require special care if the desired lock behavior is immediate. In
+ * that case we have to submit a combination of repeating and single requests so that the
+ * AF skips the initial state of the new mode's state machine and stays locks in the new
+ * mode as well.
*
* @param frameLimit the maximum number of frames to wait before we give up waiting for
* this operation to complete.
@@ -249,28 +254,6 @@
* or time limit was reached.
*/
public suspend fun lock3A(
- aeLockBehavior: Lock3ABehavior? = null,
- afLockBehavior: Lock3ABehavior? = null,
- awbLockBehavior: Lock3ABehavior? = null,
- frameLimit: Int = DEFAULT_FRAME_LIMIT,
- timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
- ): Deferred<Result3A>
-
- /**
- * Locks the auto-exposure, auto-focus and auto-whitebalance as per the given desired
- * behaviors. This method is similar to the earlier [lock3A] method with additional
- * capability of applying the given 3A parameters before the lock is obtained.
- *
- * @param frameLimit the maximum number of frames to wait before we give up waiting for
- * this operation to complete.
- * @param timeLimitMs the maximum time limit in ms we wait before we give up waiting for
- * this operation to complete.
- *
- * @return [Result3A], which will contain the latest frame number at which the locks were
- * applied or the frame number at which the method returned early because either frame limit
- * or time limit was reached.
- */
- public fun lock3A(
aeMode: AeMode? = null,
afMode: AfMode? = null,
awbMode: AwbMode? = null,
@@ -281,7 +264,7 @@
afLockBehavior: Lock3ABehavior? = null,
awbLockBehavior: Lock3ABehavior? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
- timeLimitMs: Int = DEFAULT_TIME_LIMIT_MS
+ timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
): Deferred<Result3A>
/**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
index d1d4e13..469a17d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphSessionImpl.kt
@@ -110,6 +110,12 @@
}
override suspend fun lock3A(
+ aeMode: AeMode?,
+ afMode: AfMode?,
+ awbMode: AwbMode?,
+ aeRegions: List<MeteringRectangle>?,
+ afRegions: List<MeteringRectangle>?,
+ awbRegions: List<MeteringRectangle>?,
aeLockBehavior: Lock3ABehavior?,
afLockBehavior: Lock3ABehavior?,
awbLockBehavior: Lock3ABehavior?,
@@ -121,28 +127,17 @@
// ae, af and awb respectively. If not supported return an exception or return early with
// the right status code.
return controller3A.lock3A(
- aeLockBehavior, afLockBehavior, awbLockBehavior, frameLimit,
+ aeRegions,
+ afRegions,
+ awbRegions,
+ aeLockBehavior,
+ afLockBehavior,
+ awbLockBehavior,
+ frameLimit,
timeLimitNs
)
}
- override fun lock3A(
- aeMode: AeMode?,
- afMode: AfMode?,
- awbMode: AwbMode?,
- aeRegions: List<MeteringRectangle>?,
- afRegions: List<MeteringRectangle>?,
- awbRegions: List<MeteringRectangle>?,
- aeLockBehavior: Lock3ABehavior?,
- afLockBehavior: Lock3ABehavior?,
- awbLockBehavior: Lock3ABehavior?,
- frameLimit: Int,
- timeLimitMs: Int
- ): Deferred<Result3A> {
- check(!closed.value) { "Cannot call lock3A on $this after close." }
- TODO("Implement lock3A")
- }
-
override fun unlock3A(ae: Boolean?, af: Boolean?, awb: Boolean?): Deferred<FrameNumber> {
check(!closed.value) { "Cannot call unlock3A on $this after close." }
throw UnsupportedOperationException()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 190b10f..47a94c0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -33,7 +33,6 @@
import androidx.camera.camera2.pipe.AwbMode
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
import androidx.camera.camera2.pipe.FlashMode
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Result3A
@@ -99,7 +98,7 @@
CONTROL_AE_PRECAPTURE_TRIGGER to CONTROL_AE_PRECAPTURE_TRIGGER_START
)
- private val result3ASubmitFailed = Result3A(FRAME_NUMBER_INVALID, Status.SUBMIT_FAILED)
+ private val result3ASubmitFailed = Result3A(Status.SUBMIT_FAILED)
private val aeUnlockedStateList = listOf(
CaptureResult.CONTROL_AE_STATE_INACTIVE,
@@ -202,8 +201,9 @@
}
/**
- * Given the desired lock behaviors for ae, af and awb, this method, (a) first unlocks them and
- * wait for them to converge, and then (b) locks them.
+ * Given the desired metering regions and lock behaviors for ae, af and awb, this method
+ * updates the metering regions and then, (a) unlocks ae, af, awb and wait for them to converge,
+ * and then (b) locks them.
*
* (a) In this step, as needed, we first send a single request with 'af trigger = cancel' to
* unlock af, and then a repeating request to unlock ae and awb. We suspend till we receive a
@@ -222,12 +222,20 @@
* timeLimit) to complete
*/
suspend fun lock3A(
+ aeRegions: List<MeteringRectangle>? = null,
+ afRegions: List<MeteringRectangle>? = null,
+ awbRegions: List<MeteringRectangle>? = null,
aeLockBehavior: Lock3ABehavior? = null,
afLockBehavior: Lock3ABehavior? = null,
awbLockBehavior: Lock3ABehavior? = null,
frameLimit: Int = DEFAULT_FRAME_LIMIT,
timeLimitNs: Long? = DEFAULT_TIME_LIMIT_NS
): Deferred<Result3A> {
+ // Update the 3A state of camera graph with the given metering regions. If metering regions
+ // are given as null then they are ignored and the current metering regions continue to be
+ // applied in subsequent requests to the camera device.
+ graphState3A.update(aeRegions = aeRegions, afRegions = afRegions, awbRegions = awbRegions)
+
// If we explicitly need to unlock af first before proceeding to lock it, we need to send
// a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
if (afLockBehavior.shouldUnlockAf()) {
@@ -279,8 +287,8 @@
}
val result = listener.result.await()
debug {
- "lock3A - converged at frame number=${result.frameNumber.value}, status=${result
- .status}"
+ "lock3A - converged at frame number=${result.frameMetadata?.frameNumber?.value}, " +
+ "status=${result.status}"
}
// Return immediately if we encounter an error when unlocking and waiting for
// convergence.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
index cc813c6..9641f64 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Result3AStateListener.kt
@@ -96,9 +96,7 @@
currentTimestampNs != null &&
currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs
) {
- _result.complete(
- Result3A(frameMetadata.frameNumber, Result3A.Status.TIME_LIMIT_REACHED)
- )
+ _result.complete(Result3A(Result3A.Status.TIME_LIMIT_REACHED, frameMetadata))
return true
}
@@ -110,9 +108,7 @@
if (frameNumberOfFirstUpdate != null && frameLimit != null &&
currentFrameNumber.value - frameNumberOfFirstUpdate.value > frameLimit
) {
- _result.complete(
- Result3A(frameMetadata.frameNumber, Result3A.Status.FRAME_LIMIT_REACHED)
- )
+ _result.complete(Result3A(Result3A.Status.FRAME_LIMIT_REACHED, frameMetadata))
return true
}
@@ -122,7 +118,7 @@
return false
}
}
- _result.complete(Result3A(frameMetadata.frameNumber, Result3A.Status.OK))
+ _result.complete(Result3A(Result3A.Status.OK, frameMetadata))
return true
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
index 33c4844..aae32a7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AForCaptureTest.kt
@@ -103,7 +103,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by lock3AForCapture call.
@@ -180,7 +180,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
@@ -217,7 +217,7 @@
cameraResponse.await()
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We now check if the correct sequence of requests were submitted by unlock3APostCapture
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
index ba01b9c..89e5ebb 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ALock3ATest.kt
@@ -18,6 +18,7 @@
import android.hardware.camera2.CaptureRequest
import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.params.MeteringRectangle
import android.os.Build
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.Lock3ABehavior
@@ -109,7 +110,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// We not check if the correct sequence of requests were submitted by lock3A call. The
@@ -199,7 +200,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// A single request to lock AF must have been used as well.
@@ -275,7 +276,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one more request to lock AE after new scan is done.
@@ -353,7 +354,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one request to monitor AF to finish it's scan.
@@ -433,7 +434,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
@@ -519,7 +520,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// There should be one request to monitor AF to finish it's scan.
@@ -613,7 +614,7 @@
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
// One request to cancel AF to start a new scan.
@@ -643,6 +644,98 @@
)
}
+ @Test
+ fun testLock3AWithRegions(): Unit = runBlocking {
+ initGraphProcessor()
+
+ val afMeteringRegion = MeteringRectangle(1, 1, 100, 100, 2)
+ val aeMeteringRegion = MeteringRectangle(10, 15, 140, 140, 3)
+ val result = controller3A.lock3A(
+ aeRegions = listOf(aeMeteringRegion),
+ afRegions = listOf(afMeteringRegion),
+ afLockBehavior = Lock3ABehavior.IMMEDIATE,
+ aeLockBehavior = Lock3ABehavior.IMMEDIATE
+ )
+ assertThat(result.isCompleted).isFalse()
+
+ // Since requirement of to lock both AE and AF immediately, the requests to lock AE and AF
+ // are sent right away. The result of lock3A call will complete once AE and AF have reached
+ // their desired states. In this response i.e cameraResponse1, AF is still scanning so the
+ // result won't be complete.
+ val cameraResponse = GlobalScope.async {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_PASSIVE_SCAN,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ cameraResponse.await()
+ assertThat(result.isCompleted).isFalse()
+
+ // One we we are notified that the AE and AF are in locked state, the result of lock3A call
+ // will complete.
+ GlobalScope.launch {
+ listener3A.onRequestSequenceCreated(
+ FakeRequestMetadata(
+ requestNumber = RequestNumber(1)
+ )
+ )
+ listener3A.onPartialCaptureResult(
+ FakeRequestMetadata(requestNumber = RequestNumber(1)),
+ FrameNumber(101L),
+ FakeFrameMetadata(
+ frameNumber = FrameNumber(101L),
+ resultMetadata = mapOf(
+ CaptureResult.CONTROL_AF_STATE to CaptureResult
+ .CONTROL_AF_STATE_FOCUSED_LOCKED,
+ CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_LOCKED
+ )
+ )
+ )
+ }
+
+ val result3A = result.await()
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
+
+ val aeRegions = graphState3A.aeRegions!!
+ assertThat(aeRegions.size).isEqualTo(1)
+ assertThat(aeRegions[0]).isEqualTo(aeMeteringRegion)
+
+ val afRegions = graphState3A.afRegions!!
+ assertThat(afRegions.size).isEqualTo(1)
+ assertThat(afRegions[0]).isEqualTo(afMeteringRegion)
+
+ // We not check if the correct sequence of requests were submitted by lock3A call. The
+ // request should be a repeating request to lock AE.
+ val request1 = requestProcessor.nextEvent().requestSequence
+ assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+
+ // The second request should be a single request to lock AF.
+ val request2 = requestProcessor.nextEvent().requestSequence
+ assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+ CaptureRequest.CONTROL_AF_TRIGGER_START
+ )
+ assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+ true
+ )
+ }
+
private fun initGraphProcessor() {
graphProcessor.onGraphStarted(requestProcessor)
graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
index 2e0c850..df382c3 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASetTorchTest.kt
@@ -76,7 +76,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -108,7 +108,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -144,7 +144,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
index 659d3f6..2a57464 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3ASubmit3ATest.kt
@@ -23,7 +23,6 @@
import androidx.camera.camera2.pipe.AeMode
import androidx.camera.camera2.pipe.AfMode
import androidx.camera.camera2.pipe.AwbMode
-import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestNumber
@@ -83,7 +82,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -111,7 +110,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -139,7 +138,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -167,7 +166,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -195,7 +194,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -228,7 +227,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -240,7 +239,7 @@
// Since the request processor is closed the submit3A method call will fail.
val result = controller3A.submit3A(aeMode = AeMode.ON_ALWAYS_FLASH).await()
- assertThat(result.frameNumber).isEqualTo(FRAME_NUMBER_INVALID)
+ assertThat(result.frameMetadata).isNull()
assertThat(result.status).isEqualTo(Result3A.Status.SUBMIT_FAILED)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
index 6f2e791..2e145aa 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUnlock3ATest.kt
@@ -107,7 +107,7 @@
}
val result3A = result.await()
- Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -168,7 +168,7 @@
}
val result3A = result.await()
- Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -231,7 +231,7 @@
}
val result3A = result.await()
- Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -298,7 +298,7 @@
}
val result3A = result.await()
- Truth.assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ Truth.assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
Truth.assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
index 990529b..54d025f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/Controller3AUpdate3ATest.kt
@@ -96,7 +96,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -124,7 +124,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -152,7 +152,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -180,7 +180,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -208,7 +208,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
@@ -240,7 +240,7 @@
)
}
val result3A = result.await()
- assertThat(result3A.frameNumber.value).isEqualTo(101L)
+ assertThat(result3A.frameMetadata!!.frameNumber.value).isEqualTo(101L)
assertThat(result3A.status).isEqualTo(Result3A.Status.OK)
}
diff --git a/camera/camera-camera2/api/public_plus_experimental_current.txt b/camera/camera-camera2/api/public_plus_experimental_current.txt
index ddf3bf4..4d34724 100644
--- a/camera/camera-camera2/api/public_plus_experimental_current.txt
+++ b/camera/camera-camera2/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
method public <ValueT> androidx.camera.camera2.interop.CaptureRequestOptions.Builder setCaptureRequestOption(android.hardware.camera2.CaptureRequest.Key<ValueT!>, ValueT);
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCamera2Interop {
}
}
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index 7f670ad9..5b1bc69 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
import androidx.build.LibraryGroups
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -27,10 +28,8 @@
dependencies {
api(project(":camera:camera-core"))
-
- implementation("androidx.core:core:1.1.0")
api("androidx.annotation:annotation:1.0.0")
- api("androidx.annotation:annotation-experimental:1.0.0-rc01")
+ implementation("androidx.core:core:1.1.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation(GUAVA_LISTENABLE_FUTURE)
implementation(AUTO_VALUE_ANNOTATIONS)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
deleted file mode 100644
index 1406fb1..0000000
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/UseCaseCombinationTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2;
-
-import android.content.Context;
-
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.core.CameraXConfig;
-import androidx.camera.core.ImageAnalysis;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.Preview;
-import androidx.camera.core.internal.CameraUseCaseAdapter;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.Arrays;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/**
- * Contains tests for {@link androidx.camera.core.CameraX} which varies use case combinations to
- * run.
- */
-@LargeTest
-@RunWith(AndroidJUnit4.class)
-public final class UseCaseCombinationTest {
- private static final CameraSelector DEFAULT_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA;
-
- @Rule
- public TestRule mCameraRule = CameraUtil.grantCameraPermissionAndPreTest();
-
- private Context mContext;
-
- @Before
- public void setUp() {
- mContext = ApplicationProvider.getApplicationContext();
- final CameraXConfig config = Camera2Config.defaultConfig();
- CameraX.initialize(mContext, config);
- }
-
- @After
- public void tearDown() throws InterruptedException, ExecutionException, TimeoutException {
- CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
- }
-
- /**
- * Test Combination: Preview + ImageCapture
- */
- @Test
- public void previewCombinesImageCapture() {
- final Preview preview = initPreview();
- final ImageCapture imageCapture = initImageCapture();
-
- CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
- DEFAULT_SELECTOR);
- camera.detachUseCases();
-
- // TODO(b/160249108) move off of main thread once UseCases can be attached on any
- // thread
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- // This should not throw CameraUseCaseAdapter.CameraException
- try {
- camera.addUseCases(Arrays.asList(preview, imageCapture));
- } catch (CameraUseCaseAdapter.CameraException e) {
- throw new IllegalArgumentException(e);
- }
- });
- }
-
- /**
- * Test Combination: Preview + ImageAnalysis
- */
- @Test
- public void previewCombinesImageAnalysis() {
- final Preview preview = initPreview();
- final ImageAnalysis imageAnalysis = initImageAnalysis();
-
- CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
- DEFAULT_SELECTOR);
- camera.detachUseCases();
-
- // TODO(b/160249108) move off of main thread once UseCases can be attached on any
- // thread
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- // This should not throw CameraUseCaseAdapter.CameraException
- try {
- camera.addUseCases(Arrays.asList(preview, imageAnalysis));
- } catch (CameraUseCaseAdapter.CameraException e) {
- throw new IllegalArgumentException(e);
- }
- });
- }
-
- /** Test Combination: Preview + ImageAnalysis + ImageCapture */
- @Test
- public void previewCombinesImageAnalysisAndImageCapture() {
- final Preview preview = initPreview();
- final ImageAnalysis imageAnalysis = initImageAnalysis();
- final ImageCapture imageCapture = initImageCapture();
-
- CameraUseCaseAdapter camera = CameraUtil.createCameraUseCaseAdapter(mContext,
- DEFAULT_SELECTOR);
- camera.detachUseCases();
-
- // TODO(b/160249108) move off of main thread once UseCases can be attached on any
- // thread
- InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
- // This should not throw CameraUseCaseAdapter.CameraException
- try {
- camera.addUseCases(Arrays.asList(preview, imageAnalysis, imageCapture));
- } catch (CameraUseCaseAdapter.CameraException e) {
- throw new IllegalArgumentException(e);
- }
- });
- }
-
- private Preview initPreview() {
- return new Preview.Builder().setTargetName("Preview").build();
- }
-
- private ImageAnalysis initImageAnalysis() {
- return new ImageAnalysis.Builder()
- .setTargetName("ImageAnalysis")
- .build();
- }
-
- private ImageCapture initImageCapture() {
- return new ImageCapture.Builder().build();
- }
-}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
index b0debf9..dbe9b8e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/impl/Camera2ImplConfig.java
@@ -22,9 +22,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.camera2.interop.CaptureRequestOptions;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.ExtendableBuilder;
@@ -36,7 +36,7 @@
/**
* Internal shared implementation details for camera 2 interop.
*/
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
public final class Camera2ImplConfig extends CaptureRequestOptions {
/** @hide */
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index bb39cdb..afa5645 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -33,8 +33,8 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -102,7 +102,7 @@
* requests end in {@code ImmediateFailedFuture}. Any cached requests are dropped.</li>
* </ul>
*/
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
public class Camera2CameraControlImpl implements CameraControlInternal {
private static final String TAG = "Camera2CameraControlImp";
@VisibleForTesting
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 69c20f1..41f1219 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -24,7 +24,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
import androidx.camera.camera2.interop.Camera2CameraInfo;
@@ -61,7 +61,7 @@
* CameraCaptureCallbacks added before this link will also be added
* to the {@link Camera2CameraControlImpl}.
*/
-@UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+@OptIn(markerClass = ExperimentalCamera2Interop.class)
public final class Camera2CameraInfoImpl implements CameraInfoInternal {
private static final String TAG = "Camera2CameraInfo";
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
index 99611c7..e1e385a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureOptionUnpacker.java
@@ -17,7 +17,7 @@
package androidx.camera.camera2.internal;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.impl.CaptureConfig;
@@ -33,7 +33,7 @@
static final Camera2CaptureOptionUnpacker INSTANCE = new Camera2CaptureOptionUnpacker();
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@Override
public void unpack(@NonNull UseCaseConfig<?> config,
@NonNull final CaptureConfig.Builder builder) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
index 74b2b20..59f815e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CaptureRequestBuilder.java
@@ -23,7 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.interop.CaptureRequestOptions;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.Logger;
@@ -69,7 +69,7 @@
return surfaceList;
}
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
private static void applyImplementationOptionToCaptureBuilder(
CaptureRequest.Builder builder, Config config) {
CaptureRequestOptions bundle = CaptureRequestOptions.Builder.from(config).build();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
index 577ad8c..3132339 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2SessionOptionUnpacker.java
@@ -17,7 +17,7 @@
package androidx.camera.camera2.internal;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -35,7 +35,7 @@
static final Camera2SessionOptionUnpacker INSTANCE = new Camera2SessionOptionUnpacker();
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@Override
public void unpack(@NonNull UseCaseConfig<?> config,
@NonNull final SessionConfig.Builder builder) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index a07b42b..3fa60a5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -27,7 +27,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.impl.CameraEventCallbacks;
import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
@@ -261,7 +261,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@NonNull
private ListenableFuture<Void> openCaptureSession(@NonNull List<Surface> configuredSurfaces,
@NonNull SessionConfig sessionConfig, @NonNull CameraDevice cameraDevice) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
index 12e6f7f..8b87d66 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -23,7 +23,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.internal.annotation.CameraExecutor;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -53,7 +53,7 @@
* The task will fails with {@link CameraControl.OperationCanceledException} if the camera is
* closed.
*/
-@UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+@OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
public class ExposureControl {
private static final int DEFAULT_EXPOSURE_COMPENSATION = 0;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
index f9df834..84441ee 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/interop/ExperimentalCamera2Interop.java
@@ -18,6 +18,8 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
+import androidx.annotation.RequiresOptIn;
+
import java.lang.annotation.Retention;
/**
@@ -36,10 +38,7 @@
*
* <p>These will be changed in future release possibly, hence add @Experimental annotation.
*/
-// TODO(b/170599666): Experimental/UseExperimental is deprecated and has to be replaced with
-// RequiresOptIn/OptIn.
-@SuppressWarnings("deprecation")
@Retention(CLASS)
[email protected]
+@RequiresOptIn
public @interface ExperimentalCamera2Interop {
}
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index b10368c..32b510d 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -90,25 +90,25 @@
ctor public DisplayOrientedMeteringPointFactory(android.view.Display, androidx.camera.core.CameraInfo, float, float);
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalAvailableCamerasLimiter {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraFilter {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCustomizableThreads {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalExposureCompensation {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalGetImage {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLogging {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalLogging {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroup {
}
@androidx.camera.core.ExperimentalExposureCompensation public interface ExposureState {
diff --git a/camera/camera-core/build.gradle b/camera/camera-core/build.gradle
index 4d18631..4a8792b 100644
--- a/camera/camera-core/build.gradle
+++ b/camera/camera-core/build.gradle
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
+
import androidx.build.LibraryGroups
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -26,9 +28,10 @@
dependencies {
api("androidx.annotation:annotation:1.0.0")
- api("androidx.annotation:annotation-experimental:1.0.0-rc01")
api("androidx.lifecycle:lifecycle-livedata:2.1.0")
api(GUAVA_LISTENABLE_FUTURE)
+ api("androidx.annotation:annotation-experimental:1.1.0-rc01")
+ api(KOTLIN_STDLIB) // Added for annotation-experimental
implementation("androidx.exifinterface:exifinterface:1.0.0")
implementation("androidx.core:core:1.1.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
index b1a20e5..0013339 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/AndroidImageProxyTest.java
@@ -26,7 +26,7 @@
import android.graphics.Rect;
import android.media.Image;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -129,7 +129,7 @@
}
@Test
- @UseExperimental(markerClass = ExperimentalGetImage.class)
+ @OptIn(markerClass = ExperimentalGetImage.class)
public void getImage_returnsWrappedImage() {
assertThat(mImageProxy.getImage()).isEqualTo(mImage);
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
index e40d2933..aaa9ed5 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ForwardingImageProxyTest.java
@@ -25,7 +25,7 @@
import android.graphics.ImageFormat;
import android.graphics.Rect;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
@@ -129,7 +129,7 @@
}
@Test
- @UseExperimental(markerClass = ExperimentalGetImage.class)
+ @OptIn(markerClass = ExperimentalGetImage.class)
public void getImage_returnsImageForWrappedImage() {
assertThat(mImageProxy.getImage()).isEqualTo(mBaseImageProxy.getImage());
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
index e4d5954..73e8342 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraSelector.java
@@ -18,9 +18,9 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.LensFacingCameraFilter;
@@ -94,7 +94,7 @@
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@NonNull
public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
List<CameraInfo> input = new ArrayList<>(cameraInfos);
@@ -168,7 +168,7 @@
* @hide
*/
@RestrictTo(Scope.LIBRARY_GROUP)
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@Nullable
public Integer getLensFacing() {
Integer currentLensFacing = null;
@@ -212,7 +212,7 @@
* <p>If lens facing is already set, this will add extra requirement for lens facing
* instead of replacing the previous setting.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@NonNull
public Builder requireLensFacing(@LensFacing int lensFacing) {
mCameraFilterSet.add(new LensFacingCameraFilter(lensFacing));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index 8bcdb97..0ee2ec9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -29,9 +29,9 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.impl.CameraDeviceSurfaceManager;
import androidx.camera.core.impl.CameraFactory;
import androidx.camera.core.impl.CameraInternal;
@@ -539,7 +539,7 @@
/**
* Initializes camera stack on the given thread and retry recursively until timeout.
*/
- @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
+ @OptIn(markerClass = ExperimentalAvailableCamerasLimiter.class)
private void initAndRetryRecursively(
@NonNull Executor cameraExecutor,
long startMs,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
index 99fc74f..7d03a531 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalAvailableCamerasLimiter.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -40,6 +40,6 @@
* reduce the latency.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalAvailableCamerasLimiter {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
index 1bdf1b6..7ce851b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCameraFilter.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -32,6 +32,6 @@
* available camera, the camera selector will thrown an IllegalArgumentException.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalCameraFilter {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
index cc9772d..028ef58 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalCustomizableThreads.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -36,6 +36,6 @@
* requirements.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalCustomizableThreads {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
index b7c7ce4..6e22ab7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalExposureCompensation.java
@@ -19,7 +19,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -32,6 +32,6 @@
* {@link androidx.camera.core.CameraInfo}.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalExposureCompensation {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
index 0aedde3..b84dc050 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalGetImage.java
@@ -20,7 +20,7 @@
import android.media.Image;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -37,6 +37,6 @@
* should be called on the ImageProxy from which the Image was retrieved.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalGetImage {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
index 3de235b..e2b415b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalLogging.java
@@ -16,7 +16,7 @@
package androidx.camera.core;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -27,6 +27,6 @@
* level to use inside CameraX.
*/
@Retention(RetentionPolicy.CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalLogging {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
index 216a281..eaa2bd6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ExperimentalUseCaseGroup.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -33,6 +33,6 @@
* all the use cases.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalUseCaseGroup {
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index faa196b..e9af877 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -54,11 +54,11 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.impl.CameraCaptureCallback;
import androidx.camera.core.impl.CameraCaptureResult;
import androidx.camera.core.impl.CameraInfoInternal;
@@ -192,7 +192,7 @@
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
SessionConfig.Builder createPipeline(@NonNull String cameraId, @NonNull PreviewConfig config,
@NonNull Size resolution) {
Threads.checkMainThread();
@@ -351,7 +351,7 @@
* {@link #setSurfaceProvider(SurfaceProvider)}.
*/
@UiThread
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
public void setSurfaceProvider(@NonNull Executor executor,
@Nullable SurfaceProvider surfaceProvider) {
Threads.checkMainThread();
@@ -528,7 +528,7 @@
* @hide
*/
@Override
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
@RestrictTo(Scope.LIBRARY)
public void setViewPortCropRect(@NonNull Rect viewPortCropRect) {
super.setViewPortCropRect(viewPortCropRect);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
index eb062bd..55c247b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/LensFacingCameraFilter.java
@@ -17,7 +17,7 @@
package androidx.camera.core.impl;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.core.CameraFilter;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
@@ -30,7 +30,7 @@
/**
* A filter that filters camera based on lens facing.
*/
-@UseExperimental(markerClass = ExperimentalCameraFilter.class)
+@OptIn(markerClass = ExperimentalCameraFilter.class)
public class LensFacingCameraFilter implements CameraFilter {
@CameraSelector.LensFacing
private int mLensFacing;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 79fbc5d..0619dfc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -22,7 +22,7 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraFilter;
@@ -175,7 +175,7 @@
* @throws CameraException Thrown if the combination of newly added UseCases and the
* currently added UseCases exceed the capability of the camera.
*/
- @UseExperimental(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
public void addUseCases(@NonNull Collection<UseCase> useCases) throws CameraException {
synchronized (mLock) {
List<UseCase> newUseCases = new ArrayList<>();
@@ -366,7 +366,7 @@
return suggestedResolutions;
}
- @UseExperimental(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = androidx.camera.core.ExperimentalUseCaseGroup.class)
private void updateViewPort(@NonNull Map<UseCase, Size> suggestedResolutionsMap,
@NonNull Collection<UseCase> useCases) {
synchronized (mLock) {
@@ -492,7 +492,7 @@
}
@Override
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
public void setExtendedConfig(@Nullable CameraConfig cameraConfig) throws CameraException {
synchronized (mLock) {
CameraConfig newCameraConfig = cameraConfig == null ? CameraConfigs.emptyConfig() :
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java b/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
index 02f4bc5..afd96b6 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/CameraSelectorTest.java
@@ -21,7 +21,7 @@
import android.os.Build;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.core.impl.CameraControlInternal;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.testing.fakes.FakeCamera;
@@ -98,7 +98,7 @@
cameraSelectorBuilder.build().getLensFacing();
}
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@Test
public void canAppendFilters() {
CameraFilter filter0 = mock(CameraFilter.class);
@@ -124,7 +124,7 @@
assertThat(CameraSelector.DEFAULT_FRONT_CAMERA.select(mCameras)).isEqualTo(mFrontCamera);
}
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@Test(expected = IllegalArgumentException.class)
public void exception_extraOutputCamera() {
CameraSelector.Builder cameraSelectorBuilder = new CameraSelector.Builder();
@@ -137,7 +137,7 @@
cameraSelectorBuilder.build().select(mCameras);
}
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@Test(expected = UnsupportedOperationException.class)
public void exception_extraInputCamera() {
CameraSelector.Builder cameraSelectorBuilder = new CameraSelector.Builder();
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index a42275e..90de15a 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.impl;
+import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
import android.util.Pair;
@@ -35,9 +36,9 @@
* <p> The result of the processing step should be written to the {@link Surface} that was
* received by {@link #onOutputSurface(Surface, int)}.
*
- * @param results The map of images and metadata to process. The {@link Image} that are
- * contained within the map will become invalid after this method completes,
- * so no references to them should be kept.
+ * @param results The map of {@link ImageFormat#YUV_420_888} format images and metadata to
+ * process. The {@link Image} that are contained within the map will become
+ * invalid after this method completes, so no references to them should be kept.
*/
void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
}
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
index f1817a7..2879568 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -20,6 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
/**
* Provides interfaces that the OEM needs to implement to handle the state change.
@@ -51,8 +52,9 @@
* This will be invoked before creating a
* {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
* parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
- * part of the capture session initialization via setSessionParameters(). The valid parameter
- * is a subset of the available capture request parameters.
+ * part of the capture session initialization via
+ * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported from
+ * API level 28. The valid parameter is a subset of the available capture request parameters.
*
* @return The request information to set the session wide camera parameters.
*/
diff --git a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index 629f13c..e7ecaa1 100644
--- a/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/camera-extensions-stub/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.impl;
+import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
@@ -32,8 +33,8 @@
* <p> The result of the processing step should be written to the {@link android.view.Surface}
* that was received by {@link ProcessorImpl#onOutputSurface(android.view.Surface, int)}.
*
- * @param image The image to process. This will be invalid after the method completes so no
- * reference to it should be kept.
+ * @param image The {@link ImageFormat#YUV_420_888} format image to process. This will be
+ * invalid after the method completes so no reference to it should be kept.
* @param result The metadata associated with the image to process.
*/
void process(Image image, TotalCaptureResult result);
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java
deleted file mode 100644
index 0a3e099..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.extensions.impl.ImageCaptureExtenderImpl;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(AndroidJUnit4.class)
-public class ImageCaptureExtenderValidationTest {
- @Rule
- public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- @Before
- public void setUp() throws InterruptedException, ExecutionException, TimeoutException {
- assumeTrue(CameraUtil.deviceHasCamera());
- CameraX.initialize(mContext, Camera2Config.defaultConfig());
-
- assumeTrue(ExtensionsTestUtil.initExtensions(mContext));
- }
-
- @After
- public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
- CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
- ExtensionsManager.deinit().get();
- }
-
- @Test
- @LargeTest
- public void getSupportedResolutionsImplementationTest()
- throws CameraInfoUnavailableException, CameraAccessException {
- // getSupportedResolutions supported since version 1.1
- assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0);
-
- // Uses for-loop to check all possible effect/lens facing combinations
- for (Object[] EffectLensFacingPair :
- ExtensionsTestUtil.getAllEffectLensFacingCombinations()) {
- ExtensionsManager.EffectMode effectMode =
- (ExtensionsManager.EffectMode) EffectLensFacingPair[0];
- @CameraSelector.LensFacing int lensFacing = (int) EffectLensFacingPair[1];
-
- assumeTrue(CameraUtil.hasCameraWithLensFacing(lensFacing));
- assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing));
-
- // Retrieves the target format/resolutions pair list from vendor library for the
- // target effect mode.
- ImageCaptureExtenderImpl impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(
- effectMode, lensFacing);
-
- // NoSuchMethodError will be thrown if getSupportedResolutions is not
- // implemented in vendor library, and then the test will fail.
- impl.getSupportedResolutions();
- }
- }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt
new file mode 100644
index 0000000..04abfb93
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureExtenderValidationTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions
+
+import android.content.Context
+import android.hardware.camera2.CameraAccessException
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfoUnavailableException
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.extensions.util.ExtensionsTestUtil
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@SmallTest
+@RunWith(Parameterized::class)
+class ImageCaptureExtenderValidationTest(
+ @field:Extensions.ExtensionMode @param:Extensions.ExtensionMode private val extensionMode: Int,
+ @field:CameraSelector.LensFacing @param:CameraSelector.LensFacing private val lensFacing: Int
+) {
+ private val context =
+ ApplicationProvider.getApplicationContext<Context>()
+
+ private val effectMode: ExtensionsManager.EffectMode =
+ ExtensionsTestUtil.extensionModeToEffectMode(extensionMode)
+
+ private lateinit var extensions: Extensions
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ Assume.assumeTrue(CameraUtil.deviceHasCamera())
+ CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+ Assume.assumeTrue(
+ CameraUtil.hasCameraWithLensFacing(
+ lensFacing
+ )
+ )
+ Assume.assumeTrue(ExtensionsTestUtil.initExtensions(context))
+ extensions = ExtensionsManager.getExtensions(context)
+ }
+
+ @After
+ @Throws(
+ InterruptedException::class,
+ ExecutionException::class,
+ TimeoutException::class
+ )
+ fun cleanUp() {
+ CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
+ ExtensionsManager.deinit().get()
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "extension = {0}, facing = {1}")
+ fun initParameters(): Collection<Array<Any>> =
+ ExtensionsTestUtil.getAllExtensionsLensFacingCombinations()
+ }
+
+ @Test
+ @Throws(
+ CameraInfoUnavailableException::class,
+ CameraAccessException::class
+ )
+ fun getSupportedResolutionsImplementationTest() {
+ // getSupportedResolutions supported since version 1.1
+ Assume.assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0)
+ Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+ // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
+ // from vendor library for the target effect mode.
+ val impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(effectMode, lensFacing)
+
+ // NoSuchMethodError will be thrown if getSupportedResolutions is not implemented in
+ // vendor library, and then the test will fail.
+ impl.supportedResolutions
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O_MR1)
+ @Throws(
+ CameraInfoUnavailableException::class,
+ CameraAccessException::class
+ )
+ fun returnsNullFromOnPresetSession_whenAPILevelOlderThan28() {
+ Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+ // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
+ // API level is older than 28.
+ val impl = ExtensionsTestUtil.createImageCaptureExtenderImpl(effectMode, lensFacing)
+ assertThat(impl.onPresetSession()).isNull()
+ }
+}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java
deleted file mode 100644
index 48fa332..0000000
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.extensions;
-
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfoUnavailableException;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraX;
-import androidx.camera.extensions.impl.PreviewExtenderImpl;
-import androidx.camera.extensions.util.ExtensionsTestUtil;
-import androidx.camera.testing.CameraUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-@RunWith(AndroidJUnit4.class)
-public class PreviewExtenderValidationTest {
- @Rule
- public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest();
- private final Context mContext = ApplicationProvider.getApplicationContext();
-
- @Before
- public void setUp() throws InterruptedException, ExecutionException, TimeoutException {
- assumeTrue(CameraUtil.deviceHasCamera());
- CameraX.initialize(mContext, Camera2Config.defaultConfig());
-
- assumeTrue(ExtensionsTestUtil.initExtensions(mContext));
- }
-
- @After
- public void tearDown() throws ExecutionException, InterruptedException, TimeoutException {
- CameraX.shutdown().get(10000, TimeUnit.MILLISECONDS);
- ExtensionsManager.deinit().get();
- }
-
- @Test
- @LargeTest
- public void getSupportedResolutionsImplementationTest()
- throws CameraInfoUnavailableException, CameraAccessException {
- // getSupportedResolutions supported since version 1.1
- assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0);
-
- // Uses for-loop to check all possible effect/lens facing combinations
- for (Object[] EffectLensFacingPair :
- ExtensionsTestUtil.getAllEffectLensFacingCombinations()) {
- ExtensionsManager.EffectMode effectMode =
- (ExtensionsManager.EffectMode) EffectLensFacingPair[0];
- @CameraSelector.LensFacing int lensFacing = (int) EffectLensFacingPair[1];
-
- assumeTrue(CameraUtil.hasCameraWithLensFacing(lensFacing));
- assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing));
-
- // Retrieves the target format/resolutions pair list from vendor library for the
- // target effect mode.
- PreviewExtenderImpl impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode,
- lensFacing);
-
- // NoSuchMethodError will be thrown if getSupportedResolutions is not
- // implemented in vendor library, and then the test will fail.
- impl.getSupportedResolutions();
- }
- }
-}
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt
new file mode 100644
index 0000000..a94b9ce
--- /dev/null
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewExtenderValidationTest.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.extensions
+
+import android.content.Context
+import android.hardware.camera2.CameraAccessException
+import android.os.Build
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraInfoUnavailableException
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.extensions.util.ExtensionsTestUtil
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+
+@SmallTest
+@RunWith(Parameterized::class)
+class PreviewExtenderValidationTest(
+ @field:Extensions.ExtensionMode @param:Extensions.ExtensionMode private val extensionMode: Int,
+ @field:CameraSelector.LensFacing @param:CameraSelector.LensFacing private val lensFacing: Int
+) {
+ private val context =
+ ApplicationProvider.getApplicationContext<Context>()
+
+ private val effectMode: ExtensionsManager.EffectMode =
+ ExtensionsTestUtil.extensionModeToEffectMode(extensionMode)
+
+ private lateinit var extensions: Extensions
+
+ @Before
+ @Throws(Exception::class)
+ fun setUp() {
+ Assume.assumeTrue(CameraUtil.deviceHasCamera())
+ CameraX.initialize(context, Camera2Config.defaultConfig()).get()
+ Assume.assumeTrue(
+ CameraUtil.hasCameraWithLensFacing(
+ lensFacing
+ )
+ )
+ Assume.assumeTrue(ExtensionsTestUtil.initExtensions(context))
+ extensions = ExtensionsManager.getExtensions(context)
+ }
+
+ @After
+ @Throws(
+ InterruptedException::class,
+ ExecutionException::class,
+ TimeoutException::class
+ )
+ fun cleanUp() {
+ CameraX.shutdown()[10000, TimeUnit.MILLISECONDS]
+ ExtensionsManager.deinit().get()
+ }
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "extension = {0}, facing = {1}")
+ fun initParameters(): Collection<Array<Any>> =
+ ExtensionsTestUtil.getAllExtensionsLensFacingCombinations()
+ }
+
+ @Test
+ @Throws(
+ CameraInfoUnavailableException::class,
+ CameraAccessException::class
+ )
+ fun getSupportedResolutionsImplementationTest() {
+ // getSupportedResolutions supported since version 1.1
+ Assume.assumeTrue(ExtensionVersion.getRuntimeVersion().compareTo(Version.VERSION_1_1) >= 0)
+ Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+ // Creates the ImageCaptureExtenderImpl to retrieve the target format/resolutions pair list
+ // from vendor library for the target effect mode.
+ val impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode, lensFacing)
+
+ // NoSuchMethodError will be thrown if getSupportedResolutions is not implemented in
+ // vendor library, and then the test will fail.
+ impl.supportedResolutions
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.O_MR1)
+ @Throws(
+ CameraInfoUnavailableException::class,
+ CameraAccessException::class
+ )
+ fun returnsNullFromOnPresetSession_whenAPILevelOlderThan28() {
+ Assume.assumeTrue(ExtensionsManager.isExtensionAvailable(effectMode, lensFacing))
+
+ // Creates the ImageCaptureExtenderImpl to check that onPresetSession() returns null when
+ // API level is older than 28.
+ val impl = ExtensionsTestUtil.createPreviewExtenderImpl(effectMode, lensFacing)
+ Truth.assertThat(impl.onPresetSession()).isNull()
+ }
+}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
index 6b1924d..06237ee 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ExtensionCameraFilter.java
@@ -20,7 +20,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraFilter;
@@ -38,7 +38,7 @@
* A filter that filters camera based on extender implementation. If the implementation is
* unavailable, the camera will be considered available.
*/
-@UseExperimental(markerClass = ExperimentalCameraFilter.class)
+@OptIn(markerClass = ExperimentalCameraFilter.class)
public final class ExtensionCameraFilter implements CameraFilter {
private final PreviewExtenderImpl mPreviewExtenderImpl;
private final ImageCaptureExtenderImpl mImageCaptureExtenderImpl;
@@ -59,7 +59,7 @@
mImageCaptureExtenderImpl = imageCaptureExtenderImpl;
}
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@NonNull
@Override
public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
index 9c6f01d..f70b0b3 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/Extensions.java
@@ -20,8 +20,8 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraFilter;
import androidx.camera.core.CameraInfo;
@@ -133,7 +133,7 @@
* Camera can support the list of
* UseCases for the extension.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
public void setExtension(@NonNull Camera camera, @ExtensionMode int mode) {
if (!isExtensionAvailable(camera, mode)) {
throw new IllegalArgumentException("Extension mode not supported on camera: " + mode);
@@ -189,7 +189,7 @@
* @param camera The Camera to check if it supports the extension.
* @param mode The extension mode to check
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
public boolean isExtensionAvailable(@NonNull Camera camera, @ExtensionMode int mode) {
CameraSelector cameraSelector =
new CameraSelector.Builder().addCameraFilter(getFilter(mode)).build();
@@ -204,7 +204,7 @@
return true;
}
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
private CameraFilter getFilter(@ExtensionMode int mode) {
CameraFilter filter;
try {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
index 6605fce..5a57893 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/ImageCaptureExtender.java
@@ -18,14 +18,15 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.impl.CameraEventCallback;
import androidx.camera.camera2.impl.CameraEventCallbacks;
@@ -69,7 +70,7 @@
private int mEffectMode;
private ExtensionCameraFilter mExtensionCameraFilter;
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
void init(ImageCapture.Builder builder, ImageCaptureExtenderImpl implementation,
@Extensions.ExtensionMode int effectMode) {
mBuilder = builder;
@@ -93,7 +94,7 @@
* Returns the camera specified with the given camera selector and this extension, null if
* there's no available can be found.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
private String getCameraWithExtension(@NonNull CameraSelector cameraSelector) {
CameraSelector.Builder extensionCameraSelectorBuilder =
CameraSelector.Builder.fromSelector(cameraSelector);
@@ -118,7 +119,7 @@
* @param cameraSelector The selector used to determine the camera for which to enable
* extensions.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
public void enableExtension(@NonNull CameraSelector cameraSelector) {
String cameraId = getCameraWithExtension(cameraSelector);
if (cameraId == null) {
@@ -267,7 +268,7 @@
mContext = context;
}
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@Override
public void onAttach(@NonNull CameraInfo cameraInfo) {
if (mActive.get()) {
@@ -301,7 +302,15 @@
if (mActive.get()) {
CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+ if (Build.VERSION.SDK_INT >= 28) {
+ return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+ } else {
+ Logger.w(TAG, "The CaptureRequest parameters returned from "
+ + "onPresetSession() will be passed to the camera device as part "
+ + "of the capture session via "
+ + "SessionConfiguration#setSessionParameters(CaptureRequest) "
+ + "which only supported from API level 28!");
+ }
}
}
return null;
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
index 4a40b8a..19826ba 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/PreviewExtender.java
@@ -18,14 +18,15 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.camera2.impl.Camera2ImplConfig;
import androidx.camera.camera2.impl.CameraEventCallback;
import androidx.camera.camera2.impl.CameraEventCallbacks;
@@ -66,7 +67,7 @@
private int mEffectMode;
private ExtensionCameraFilter mExtensionCameraFilter;
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
void init(Preview.Builder builder, PreviewExtenderImpl implementation,
@Extensions.ExtensionMode int effectMode) {
mBuilder = builder;
@@ -90,7 +91,7 @@
* Returns the camera specified with the given camera selector and this extension, null if
* there's no available can be found.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
private String getCameraWithExtension(@NonNull CameraSelector cameraSelector) {
CameraSelector.Builder extensionCameraSelectorBuilder =
CameraSelector.Builder.fromSelector(cameraSelector);
@@ -115,7 +116,7 @@
* @param cameraSelector The selector used to determine the camera for which to enable
* extensions.
*/
- @UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
public void enableExtension(@NonNull CameraSelector cameraSelector) {
String cameraId = getCameraWithExtension(cameraSelector);
if (cameraId == null) {
@@ -279,7 +280,7 @@
mCloseableProcessor = closeableProcessor;
}
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
@Override
public void onAttach(@NonNull CameraInfo cameraInfo) {
synchronized (mLock) {
@@ -320,7 +321,15 @@
synchronized (mLock) {
CaptureStageImpl captureStageImpl = mImpl.onPresetSession();
if (captureStageImpl != null) {
- return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+ if (Build.VERSION.SDK_INT >= 28) {
+ return new AdaptingCaptureStage(captureStageImpl).getCaptureConfig();
+ } else {
+ Logger.w(TAG, "The CaptureRequest parameters returned from "
+ + "onPresetSession() will be passed to the camera device as part "
+ + "of the capture session via "
+ + "SessionConfiguration#setSessionParameters(CaptureRequest) "
+ + "which only supported from API level 28!");
+ }
}
}
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index 2e0c377..c653ed5 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -20,7 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraInfo;
@@ -46,7 +46,7 @@
@Extensions.ExtensionMode
private int mEffectMode;
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
public ImageCaptureConfigProvider(@Extensions.ExtensionMode int mode,
@NonNull CameraInfo cameraInfo, @NonNull Context context) {
try {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index e70da40..384f169 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -20,7 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraInfo;
@@ -46,7 +46,7 @@
@Extensions.ExtensionMode
private int mEffectMode;
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
public PreviewConfigProvider(@Extensions.ExtensionMode int mode,
@NonNull CameraInfo cameraInfo, @NonNull Context context) {
try {
diff --git a/camera/camera-lifecycle/api/public_plus_experimental_current.txt b/camera/camera-lifecycle/api/public_plus_experimental_current.txt
index 47b2878..cdd3299 100644
--- a/camera/camera-lifecycle/api/public_plus_experimental_current.txt
+++ b/camera/camera-lifecycle/api/public_plus_experimental_current.txt
@@ -1,10 +1,10 @@
// Signature format: 4.0
package androidx.camera.lifecycle {
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalCameraProviderConfiguration {
}
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroupLifecycle {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalUseCaseGroupLifecycle {
}
public final class ProcessCameraProvider implements androidx.camera.core.CameraProvider {
diff --git a/camera/camera-lifecycle/build.gradle b/camera/camera-lifecycle/build.gradle
index 57fe2aa..69a63fd 100644
--- a/camera/camera-lifecycle/build.gradle
+++ b/camera/camera-lifecycle/build.gradle
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
import androidx.build.LibraryGroups
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -31,6 +32,7 @@
api(project(":camera:camera-core"))
implementation("androidx.core:core:1.1.0")
implementation(AUTO_VALUE_ANNOTATIONS)
+
annotationProcessor(AUTO_VALUE)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
index b15354d..bedde47 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalCameraProviderConfiguration.java
@@ -18,6 +18,8 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
+import androidx.annotation.RequiresOptIn;
+
import java.lang.annotation.Retention;
/**
@@ -33,8 +35,7 @@
*
* @see androidx.camera.core.CameraXConfig.Provider
*/
-@SuppressWarnings("deprecation")
@Retention(CLASS)
[email protected]
+@RequiresOptIn
public @interface ExperimentalCameraProviderConfiguration {
}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
index 6c88ceb..dfc880d 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ExperimentalUseCaseGroupLifecycle.java
@@ -18,6 +18,8 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
+import androidx.annotation.RequiresOptIn;
+
import java.lang.annotation.Retention;
/**
@@ -32,8 +34,7 @@
*
* TODO(b/159033688): Remove after the bug is fixed.
*/
-@SuppressWarnings("deprecation")
@Retention(CLASS)
[email protected]
+@RequiresOptIn
public @interface ExperimentalUseCaseGroupLifecycle {
}
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
index 702ba88..9b2455d 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.java
@@ -23,6 +23,7 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.Camera;
@@ -272,7 +273,7 @@
@SuppressWarnings({"lambdaLast", "deprecation"})
@MainThread
@NonNull
- @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
@NonNull CameraSelector cameraSelector,
@NonNull UseCase... useCases) {
@@ -295,7 +296,7 @@
@SuppressWarnings({"lambdaLast", "deprecation"})
@MainThread
@NonNull
- @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner,
@NonNull CameraSelector cameraSelector,
@NonNull UseCaseGroup useCaseGroup) {
@@ -364,7 +365,7 @@
@SuppressWarnings({"lambdaLast", "unused", "deprecation"})
@RestrictTo(Scope.LIBRARY_GROUP)
@ExperimentalUseCaseGroup
- @androidx.annotation.experimental.UseExperimental(markerClass = ExperimentalCameraFilter.class)
+ @OptIn(markerClass = ExperimentalCameraFilter.class)
@NonNull
public Camera bindToLifecycle(
@NonNull LifecycleOwner lifecycleOwner,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 2f31568..997def5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -18,8 +18,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.CameraXConfig;
import androidx.camera.core.ExperimentalAvailableCamerasLimiter;
@@ -39,7 +39,7 @@
private static final String CAMERA_ID_1 = "1";
/** Generates a fake {@link CameraXConfig}. */
- @UseExperimental(markerClass = ExperimentalAvailableCamerasLimiter.class)
+ @OptIn(markerClass = ExperimentalAvailableCamerasLimiter.class)
@NonNull
public static CameraXConfig create() {
return create(null);
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
new file mode 100644
index 0000000..694c4b8
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/OutputOptionsTest.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.os.ParcelFileDescriptor
+import android.provider.MediaStore
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.io.FileDescriptor
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class OutputOptionsTest {
+
+ @Test
+ fun canBuildFileOutputOptions() {
+ val savedFile = File.createTempFile("CameraX", ".tmp")
+ savedFile.deleteOnExit()
+
+ val fileOutputOptions = FileOutputOptions.builder()
+ .setFile(savedFile)
+ .setFileSizeLimit(0)
+ .build()
+
+ assertThat(fileOutputOptions).isNotNull()
+ assertThat(fileOutputOptions.type).isEqualTo(OutputOptions.Type.FILE)
+ assertThat(fileOutputOptions.file).isNotNull()
+ assertThat(fileOutputOptions.fileSizeLimit).isEqualTo(0)
+ savedFile.delete()
+ }
+
+ @Test
+ fun canBuildMediaStoreOutputOptions() {
+ val context: Context = ApplicationProvider.getApplicationContext()
+ val contentResolver: ContentResolver = context.contentResolver
+ val fileName = "OutputOptionTest"
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+ put(MediaStore.Video.Media.TITLE, fileName)
+ put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
+ }
+
+ val uri = contentResolver.insert(
+ MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+
+ assertThat(uri).isNotNull()
+
+ val mediaStoreOutputOptions = MediaStoreOutputOptions.builder()
+ .setContentResolver(contentResolver)
+ .setFileSizeLimit(0)
+ .setUri(uri!!)
+ .build()
+
+ assertThat(mediaStoreOutputOptions.uri).isNotNull()
+ assertThat(mediaStoreOutputOptions.type).isEqualTo(OutputOptions.Type.MEDIA_STORE)
+ assertThat(mediaStoreOutputOptions.fileSizeLimit).isEqualTo(0)
+ contentResolver.delete(uri, null, null)
+ }
+
+ @Test
+ fun canBuildFileDescriptorOutputOptions() {
+ val savedFile = File.createTempFile("CameraX", ".tmp")
+ savedFile.deleteOnExit()
+ val pfd: ParcelFileDescriptor = ParcelFileDescriptor.open(
+ savedFile,
+ ParcelFileDescriptor.MODE_READ_WRITE
+ )
+ val fd: FileDescriptor = pfd.fileDescriptor
+
+ val fileOutputOptions = FileDescriptorOutputOptions.builder()
+ .setFileDescriptor(fd)
+ .setFileSizeLimit(0)
+ .build()
+
+ assertThat(fileOutputOptions).isNotNull()
+ assertThat(fileOutputOptions.type).isEqualTo(OutputOptions.Type.FILE_DESCRIPTOR)
+ assertThat(fileOutputOptions.fileDescriptor).isNotNull()
+ assertThat(fileOutputOptions.fileSizeLimit).isEqualTo(0)
+ pfd.close()
+ savedFile.delete()
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
new file mode 100644
index 0000000..99663f9
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileDescriptorOutputOptions.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.FileDescriptor;
+
+/**
+ * A class to store the result to a given file descriptor.
+ *
+ * <p> The file descriptor must be seekable and writable. And the caller should be responsible for
+ * closing the file descriptor.
+ */
+@AutoValue
+public abstract class FileDescriptorOutputOptions extends OutputOptions {
+
+ FileDescriptorOutputOptions() {
+ super(Type.FILE_DESCRIPTOR);
+ }
+
+ /** Returns a builder for this FileDescriptorOutputOptions. */
+ @NonNull
+ public static Builder builder() {
+ return new AutoValue_FileDescriptorOutputOptions.Builder();
+ }
+
+ /**
+ * Gets the limit for the file length in bytes.
+ */
+ @Override
+ public abstract int getFileSizeLimit();
+
+ /** Gets the FileDescriptor instance */
+ @NonNull
+ public abstract FileDescriptor getFileDescriptor();
+
+ /** The builder of the {@link FileDescriptorOutputOptions}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ Builder() {
+ }
+
+ /** Defines how to store the result. */
+ @NonNull
+ public abstract Builder setFileDescriptor(
+ @NonNull FileDescriptor fileDescriptor);
+
+ /** Sets the limit for the file length in bytes. */
+ @NonNull
+ public abstract Builder setFileSizeLimit(int bytes);
+
+ /** Builds the FileDescriptorOutputOptions instance. */
+ @NonNull
+ public abstract FileDescriptorOutputOptions build();
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
new file mode 100644
index 0000000..5c208ba
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/FileOutputOptions.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+import java.io.File;
+
+/**
+ * A class to store the result to a given file.
+ *
+ * <p> The file could be in a path where the application has permission to write in.
+ */
+@AutoValue
+public abstract class FileOutputOptions extends OutputOptions {
+
+ FileOutputOptions() {
+ super(Type.FILE);
+ }
+
+ /** Returns a builder for this FileOutputOptions. */
+ @NonNull
+ public static Builder builder() {
+ return new AutoValue_FileOutputOptions.Builder();
+ }
+
+ /**
+ * Gets the limit for the file length in bytes.
+ */
+ @Override
+ public abstract int getFileSizeLimit();
+
+ /** Gets the File instance */
+ @NonNull
+ public abstract File getFile();
+
+ /** The builder of the {@link FileOutputOptions}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ Builder() {
+ }
+
+ /** Defines how to store the result. */
+ @NonNull
+ public abstract Builder setFile(@NonNull File file);
+
+ /** Sets the limit for the file length in bytes. */
+ @NonNull
+ public abstract Builder setFileSizeLimit(int bytes);
+
+ /** Builds the FileOutputOptions instance. */
+ @NonNull
+ public abstract FileOutputOptions build();
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
new file mode 100644
index 0000000..fb08bdb
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/MediaStoreOutputOptions.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import static androidx.camera.video.OutputOptions.Type.MEDIA_STORE;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * A class provides a option for storing output to MediaStore.
+ *
+ * <p> The result could be saved to a shared storage. The results will remain on the device after
+ * the app is uninstalled.
+ */
+@AutoValue
+public abstract class MediaStoreOutputOptions extends OutputOptions {
+
+ MediaStoreOutputOptions() {
+ super(MEDIA_STORE);
+ }
+
+ /** Returns a builder for this MediaStoreOutputOptions. */
+ @NonNull
+ public static Builder builder() {
+ return new AutoValue_MediaStoreOutputOptions.Builder();
+ }
+
+ /**
+ * Gets the ContentResolver instance in order to convert Uri to a file path.
+ */
+ @NonNull
+ public abstract ContentResolver getContentResolver();
+
+ /**
+ * Gets the limit for the file length in bytes.
+ */
+ @Override
+ public abstract int getFileSizeLimit();
+
+ /** Gets the Uri instance */
+ @NonNull
+ public abstract Uri getUri();
+
+ /** The builder of the {@link MediaStoreOutputOptions}. */
+ @AutoValue.Builder
+ public abstract static class Builder {
+ Builder() {
+ }
+
+ /** Sets the ContentResolver instance. */
+ @NonNull
+ public abstract Builder setContentResolver(@NonNull ContentResolver contentResolver);
+
+ /** Defines how to store the result. */
+ @NonNull
+ public abstract Builder setUri(@NonNull Uri uri);
+
+ /** Sets the limit for the file length in bytes. */
+ @NonNull
+ public abstract Builder setFileSizeLimit(int bytes);
+
+ /** Builds the MediaStoreOutputOptions instance. */
+ @NonNull
+ public abstract MediaStoreOutputOptions build();
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
new file mode 100644
index 0000000..62e3dee
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/OutputOptions.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Options for configuring output destination.
+ */
+public abstract class OutputOptions {
+
+ Type mType;
+
+ public OutputOptions(@NonNull Type type) {
+ mType = type;
+ }
+
+ /**
+ * To be used to cast OutputOptions to subtype.
+ */
+ Type getType() {
+ return mType;
+ }
+
+ /**
+ * Gets the limit for the file length in bytes.
+ */
+ public abstract int getFileSizeLimit();
+
+ /**
+ * Types of the output options.
+ */
+ enum Type {
+ FILE, FILE_DESCRIPTOR, MEDIA_STORE
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
index 9e69832..20e5387 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
@@ -31,7 +31,7 @@
*
* <p>For example, "video/avc" for a video encoder and "audio/mp4a-latm" for an audio encoder.
*
- * @See {@link MediaFormat}
+ * @see {@link MediaFormat}
*/
@NonNull
String getMimeType();
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java
new file mode 100644
index 0000000..fa975f8
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/OutputUtil.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.utils;
+
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.Logger;
+
+/**
+ * Utility class for output related operations.
+ */
+public final class OutputUtil {
+ private static final String TAG = "OutputUtil";
+
+ private OutputUtil(){}
+
+ /** Gets the absolute path from a Uri. */
+ @Nullable
+ public static String getAbsolutePathFromUri(@NonNull ContentResolver resolver,
+ @NonNull Uri contentUri, @NonNull String mediaStoreColumn) {
+ Cursor cursor = null;
+ try {
+ String[] proj;
+ int columnIndex;
+ proj = new String[]{mediaStoreColumn};
+ cursor = resolver.query(contentUri, proj, null, null, null);
+
+ if (cursor == null) {
+ return null;
+ }
+
+ columnIndex = cursor.getColumnIndexOrThrow(mediaStoreColumn);
+ cursor.moveToFirst();
+ return cursor.getString(columnIndex);
+ } catch (RuntimeException e) {
+ Logger.e(TAG, String.format(
+ "Failed in getting absolute path for Uri %s with Exception %s",
+ contentUri.toString(), e.toString()));
+ return null;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+}
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index be9d4ae..6dbb420 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -127,7 +127,7 @@
package androidx.camera.view.video {
- @experimental.Experimental @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
+ @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
}
@androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class Metadata {
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index 1ad6270..aef81d0 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryVersions
+
import androidx.build.LibraryGroups
+import androidx.build.LibraryVersions
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
@@ -28,15 +30,19 @@
apply(from: "dependencies.gradle")
dependencies {
- implementation("androidx.appcompat:appcompat:1.1.0")
api("androidx.lifecycle:lifecycle-common:2.0.0")
+ api("androidx.annotation:annotation:1.0.0")
api("androidx.camera:camera-core:${VIEW_ATOMIC_GROUP_PINNED_VER}")
implementation("androidx.camera:camera-lifecycle:${VIEW_ATOMIC_GROUP_PINNED_VER}")
- api("androidx.annotation:annotation:1.0.0")
+ implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
implementation(GUAVA_LISTENABLE_FUTURE)
implementation("androidx.core:core:1.1.0")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
implementation(AUTO_VALUE_ANNOTATIONS)
+ implementation("androidx.appcompat:appcompat:1.1.0")
+ // Added for annotation-experimental
+ compileOnly(KOTLIN_STDLIB)
+
annotationProcessor(AUTO_VALUE)
testImplementation(ANDROIDX_TEST_RUNNER)
diff --git a/camera/camera-view/dependencies.gradle b/camera/camera-view/dependencies.gradle
index 5795377..ce7090a 100644
--- a/camera/camera-view/dependencies.gradle
+++ b/camera/camera-view/dependencies.gradle
@@ -16,5 +16,5 @@
ext {
// camera-view temporarily pins same-group depenencies to RC/stable until beta
- VIEW_ATOMIC_GROUP_PINNED_VER = "1.0.0-rc03"
+ VIEW_ATOMIC_GROUP_PINNED_VER = "1.0.0-rc04"
}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
index ba1c76f..9bc5023 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
@@ -113,7 +113,7 @@
SURFACE_SIZE,
BACK_CAMERA
)
- return mPreviewTransform.isCropRectAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
+ return mPreviewTransform.isViewportAspectRatioMatchPreviewView(PREVIEW_VIEW_SIZE)
}
@Test
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index a5f703d..87a66489 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -28,9 +28,9 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
@@ -113,7 +113,7 @@
*
* @hide
*/
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY)
@IntDef(flag = true, value = {IMAGE_CAPTURE, IMAGE_ANALYSIS, VIDEO_CAPTURE})
@@ -330,7 +330,7 @@
* @see ImageAnalysis
*/
@MainThread
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
public void setEnabledUseCases(@UseCases int enabledUseCases) {
Threads.checkMainThread();
if (enabledUseCases == mEnabledUseCases) {
@@ -359,9 +359,9 @@
* Same as {@link #isVideoCaptureEnabled()}.
*
* <p> This wrapper method is to workaround the limitation that currently only one
- * {@link UseExperimental} mark class is allowed per method.
+ * UseExperimental mark class is allowed per method.
*/
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private boolean isVideoCaptureEnabledInternal() {
return isVideoCaptureEnabled();
}
@@ -375,7 +375,7 @@
*/
@SuppressLint({"MissingPermission", "WrongConstant"})
@MainThread
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
void attachPreviewSurface(@NonNull Preview.SurfaceProvider surfaceProvider,
@NonNull ViewPort viewPort, @NonNull Display display) {
Threads.checkMainThread();
@@ -1125,7 +1125,7 @@
*/
@Nullable
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
protected UseCaseGroup createUseCaseGroup() {
if (!isCameraInitialized()) {
Logger.d(TAG, CAMERA_NOT_INITIALIZED);
@@ -1182,7 +1182,7 @@
@SuppressLint("WrongConstant")
@Override
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
public void onDisplayChanged(int displayId) {
if (mPreviewDisplay != null && mPreviewDisplay.getDisplayId() == displayId) {
mPreview.setTargetRotation(mPreviewDisplay.getRotation());
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
index 757c27ee..c1ad936 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraXModule.java
@@ -26,8 +26,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresPermission;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraInfoUnavailableException;
@@ -162,7 +162,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
@RequiresPermission(permission.CAMERA)
void bindToLifecycleAfterViewMeasured() {
if (mNewLifecycle == null) {
@@ -270,7 +270,7 @@
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
}
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
public void takePicture(Executor executor, OnImageCapturedCallback callback) {
if (mImageCapture == null) {
return;
@@ -287,7 +287,7 @@
mImageCapture.takePicture(executor, callback);
}
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor, OnImageSavedCallback callback) {
if (mImageCapture == null) {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
index e468fb2..b686559 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/LifecycleCameraController.java
@@ -26,9 +26,9 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.Camera;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
@@ -110,7 +110,7 @@
*
* @return null if failed to start camera.
*/
- @UseExperimental(markerClass = ExperimentalUseCaseGroupLifecycle.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroupLifecycle.class)
@RequiresPermission(Manifest.permission.CAMERA)
@Override
@Nullable
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index ce02f39..7944a06 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -48,14 +48,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.ExperimentalUseCaseGroup;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.ViewPort;
import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
-import androidx.camera.view.internal.compat.quirk.PreviewStretchedQuirk;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
import androidx.core.util.Preconditions;
/**
@@ -104,8 +104,12 @@
// SurfaceRequest.getResolution().
private Size mResolution;
- // TransformationInfo.getCropRect().
+ // This represents the area of the Surface that should be visible to end users. The value
+ // is based on TransformationInfo.getCropRect() with possible corrections due to device quirks.
private Rect mSurfaceCropRect;
+ // This rect represents the size of the viewport in preview. It's always the same as
+ // TransformationInfo.getCropRect().
+ private Rect mViewportRect;
// TransformationInfo.getRotationDegrees().
private int mPreviewRotationDegrees;
// TransformationInfo.getTargetRotation.
@@ -123,12 +127,13 @@
*
* <p> All the values originally come from a {@link SurfaceRequest}.
*/
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
void setTransformationInfo(@NonNull SurfaceRequest.TransformationInfo transformationInfo,
Size resolution, boolean isFrontCamera) {
Logger.d(TAG, "Transformation info set: " + transformationInfo + " " + resolution + " "
+ isFrontCamera);
- mSurfaceCropRect = transformationInfo.getCropRect();
+ mSurfaceCropRect = getCorrectedCropRect(transformationInfo.getCropRect());
+ mViewportRect = transformationInfo.getCropRect();
mPreviewRotationDegrees = transformationInfo.getRotationDegrees();
mTargetRotation = transformationInfo.getTargetRotation();
mResolution = resolution;
@@ -237,7 +242,7 @@
// Get the target of the mapping, the vertices of the crop rect in PreviewView.
float[] previewViewCropRectVertices;
- if (isCropRectAspectRatioMatchPreviewView(previewViewSize)) {
+ if (isViewportAspectRatioMatchPreviewView(previewViewSize)) {
// If crop rect has the same aspect ratio as PreviewView, scale the crop rect to fill
// the entire PreviewView. This happens if the scale type is FILL_* AND a
// PreviewView-based viewport is used.
@@ -245,7 +250,7 @@
} else {
// If the aspect ratios don't match, it could be 1) scale type is FIT_*, 2) the
// Viewport is not based on the PreviewView or 3) both.
- RectF previewViewCropRect = getPreviewViewCropRectForMismatchedAspectRatios(
+ RectF previewViewCropRect = getPreviewViewViewportRectForMismatchedAspectRatios(
previewViewSize, layoutDirection);
previewViewCropRectVertices = rectToVertices(previewViewCropRect);
}
@@ -253,7 +258,7 @@
previewViewCropRectVertices, mPreviewRotationDegrees);
// Get the source of the mapping, the vertices of the crop rect in Surface.
- float[] surfaceCropRectVertices = getSurfaceCropRectVertices();
+ float[] surfaceCropRectVertices = rectToVertices(new RectF(mSurfaceCropRect));
// Map source to target.
matrix.setPolyToPoly(surfaceCropRectVertices, 0, rotatedPreviewViewCropRectVertices, 0, 4);
@@ -282,43 +287,46 @@
/**
* Gets the vertices of the crop rect in Surface.
*/
- private float[] getSurfaceCropRectVertices() {
- RectF cropRectF = new RectF(mSurfaceCropRect);
- PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+ private Rect getCorrectedCropRect(Rect surfaceCropRect) {
+ PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
if (quirk != null) {
// Correct crop rect if the device has a quirk.
+ RectF cropRectF = new RectF(surfaceCropRect);
Matrix correction = new Matrix();
correction.setScale(
quirk.getCropRectScaleX(),
- quirk.getCropRectScaleY(),
- mSurfaceCropRect.centerX(),
- mSurfaceCropRect.centerY());
+ 1f,
+ surfaceCropRect.centerX(),
+ surfaceCropRect.centerY());
correction.mapRect(cropRectF);
+ Rect correctRect = new Rect();
+ cropRectF.round(correctRect);
+ return correctRect;
}
- return rectToVertices(cropRectF);
+ return surfaceCropRect;
}
/**
- * Gets the crop rect in {@link PreviewView} coordinates for the case where crop rect's aspect
- * ratio doesn't match {@link PreviewView}'s aspect ratio.
+ * Gets the viewport rect in {@link PreviewView} coordinates for the case where viewport's
+ * aspect ratio doesn't match {@link PreviewView}'s aspect ratio.
*
* <p> When aspect ratios don't match, additional calculation is needed to figure out how to
* fit crop rect into the{@link PreviewView}.
*/
- RectF getPreviewViewCropRectForMismatchedAspectRatios(Size previewViewSize,
+ RectF getPreviewViewViewportRectForMismatchedAspectRatios(Size previewViewSize,
int layoutDirection) {
RectF previewViewRect = new RectF(0, 0, previewViewSize.getWidth(),
previewViewSize.getHeight());
- Size rotatedCropRectSize = getRotatedCropRectSize();
- RectF rotatedSurfaceCropRect = new RectF(0, 0, rotatedCropRectSize.getWidth(),
- rotatedCropRectSize.getHeight());
+ Size rotatedViewportSize = getRotatedViewportSize();
+ RectF rotatedViewportRect = new RectF(0, 0, rotatedViewportSize.getWidth(),
+ rotatedViewportSize.getHeight());
Matrix matrix = new Matrix();
- setMatrixRectToRect(matrix, rotatedSurfaceCropRect, previewViewRect, mScaleType);
- matrix.mapRect(rotatedSurfaceCropRect);
+ setMatrixRectToRect(matrix, rotatedViewportRect, previewViewRect, mScaleType);
+ matrix.mapRect(rotatedViewportRect);
if (layoutDirection == LayoutDirection.RTL) {
- return flipHorizontally(rotatedSurfaceCropRect, (float) previewViewSize.getWidth() / 2);
+ return flipHorizontally(rotatedViewportRect, (float) previewViewSize.getWidth() / 2);
}
- return rotatedSurfaceCropRect;
+ return rotatedViewportRect;
}
/**
@@ -374,29 +382,29 @@
}
/**
- * Returns crop rect size with target rotation applied.
+ * Returns viewport size with target rotation applied.
*/
- private Size getRotatedCropRectSize() {
- Preconditions.checkNotNull(mSurfaceCropRect);
+ private Size getRotatedViewportSize() {
if (is90or270(mPreviewRotationDegrees)) {
- return new Size(mSurfaceCropRect.height(), mSurfaceCropRect.width());
+ return new Size(mViewportRect.height(), mViewportRect.width());
}
- return new Size(mSurfaceCropRect.width(), mSurfaceCropRect.height());
+ return new Size(mViewportRect.width(), mViewportRect.height());
}
/**
- * Checks if the crop rect's aspect ratio matches that of the {@link PreviewView}.
+ * Checks if the viewport's aspect ratio matches that of the {@link PreviewView}.
*
* <p> The mismatch could happen if the {@link ViewPort} is not based on the
* {@link PreviewView}, or the {@link PreviewView#getScaleType()} is FIT_*. In this case, we
* need to calculate how the crop rect should be fitted.
*/
@VisibleForTesting
- boolean isCropRectAspectRatioMatchPreviewView(Size previewViewSize) {
- Size rotatedSize = getRotatedCropRectSize();
+ boolean isViewportAspectRatioMatchPreviewView(Size previewViewSize) {
+ // Using viewport rect to check if the viewport is based on the PreviewView.
+ Size rotatedViewportSize = getRotatedViewportSize();
return isAspectRatioMatchingWithRoundingError(
previewViewSize, /* isAccurate1= */ true,
- rotatedSize, /* isAccurate2= */ false);
+ rotatedViewportSize, /* isAccurate2= */ false);
}
/**
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
index 3b44754..1983460 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewView.java
@@ -44,10 +44,10 @@
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
@@ -64,6 +64,8 @@
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.utils.Threads;
+import androidx.camera.view.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
import androidx.camera.view.transform.CoordinateTransform;
import androidx.camera.view.transform.OutputTransform;
import androidx.core.content.ContextCompat;
@@ -154,7 +156,7 @@
@SuppressWarnings("WeakerAccess")
final Preview.SurfaceProvider mSurfaceProvider = new Preview.SurfaceProvider() {
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
@Override
@AnyThread
public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
@@ -362,7 +364,7 @@
*/
@UiThread
@NonNull
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
public Preview.SurfaceProvider getSurfaceProvider() {
Threads.checkMainThread();
return mSurfaceProvider;
@@ -594,14 +596,16 @@
// Synthetic access
@SuppressWarnings("WeakerAccess")
- boolean shouldUseTextureView(@NonNull SurfaceRequest surfaceRequest,
+ static boolean shouldUseTextureView(@NonNull SurfaceRequest surfaceRequest,
@NonNull final ImplementationMode implementationMode) {
// TODO(b/159127402): use TextureView if target rotation is not display rotation.
boolean isLegacyDevice = surfaceRequest.getCamera().getCameraInfoInternal()
.getImplementationType().equals(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
- if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice) {
+ boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null;
+ if (surfaceRequest.isRGBA8888Required() || Build.VERSION.SDK_INT <= 24 || isLegacyDevice
+ || hasSurfaceViewQuirk) {
// Force to use TextureView when the device is running android 7.0 and below, legacy
- // level or RGBA8888 is required.
+ // level, RGBA8888 is required or SurfaceView has quirks.
return true;
}
switch (implementationMode) {
@@ -884,7 +888,7 @@
surfaceCropRect.height()));
}
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
private void attachToControllerIfReady(boolean shouldFailSilently) {
Display display = getDisplay();
ViewPort viewPort = getViewPort();
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java b/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
index 06ec9ef..858827d 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/TransformExperimental.java
@@ -18,6 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
+import androidx.annotation.RequiresOptIn;
import androidx.annotation.RestrictTo;
import java.lang.annotation.Retention;
@@ -34,6 +35,6 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Retention(CLASS)
[email protected]
+@RequiresOptIn
public @interface TransformExperimental {
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
index 65e35b3..d73a851 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/DeviceQuirksLoader.java
@@ -39,8 +39,12 @@
final List<Quirk> quirks = new ArrayList<>();
// Load all device specific quirks
- if (PreviewStretchedQuirk.load()) {
- quirks.add(new PreviewStretchedQuirk());
+ if (PreviewOneThirdWiderQuirk.load()) {
+ quirks.add(new PreviewOneThirdWiderQuirk());
+ }
+
+ if (SurfaceViewStretchedQuirk.load()) {
+ quirks.add(new SurfaceViewStretchedQuirk());
}
return quirks;
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
new file mode 100644
index 0000000..316e4db
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirk.java
@@ -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.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where the preview buffer is stretched.
+ *
+ * <p> The symptom is, the preview's FOV is always 1/3 wider than intended. For example, if the
+ * preview Surface is 800x600, it's actually has a FOV of 1066x600 with the same center point,
+ * but squeezed to fit the 800x600 buffer.
+ */
+public class PreviewOneThirdWiderQuirk implements Quirk {
+
+ private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
+ private static final String SAMSUNG_J5_PRIME = "ON5XELTE"; // b/183329599
+
+ static boolean load() {
+ boolean isSamsungJ5PrimeAndApi26 =
+ SAMSUNG_J5_PRIME.equals(Build.DEVICE.toUpperCase()) && Build.VERSION.SDK_INT >= 26;
+ boolean isSamsungA3 = SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase());
+ return isSamsungJ5PrimeAndApi26 || isSamsungA3;
+ }
+
+ /**
+ * The mount that the crop rect needs to be scaled in x.
+ */
+ public float getCropRectScaleX() {
+ return 0.75f;
+ }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
deleted file mode 100644
index fd274fa..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirk.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.internal.compat.quirk;
-
-import android.os.Build;
-
-import androidx.camera.core.impl.Quirk;
-
-import java.util.Arrays;
-import java.util.List;
-
-/**
- * A quirk where the preview buffer is stretched.
- *
- * <p> This is similar to the SamsungPreviewTargetAspectRatioQuirk in camera-camera2 artifact.
- * The difference is, the other quirk can be fixed by choosing a different resolution,
- * while for this one the preview is always stretched no matter what resolution is selected.
- */
-public class PreviewStretchedQuirk implements Quirk {
-
- private static final String SAMSUNG_A3_2017 = "A3Y17LTE"; // b/180121821
-
- private static final List<String> KNOWN_AFFECTED_DEVICES = Arrays.asList(SAMSUNG_A3_2017);
-
- static boolean load() {
- return KNOWN_AFFECTED_DEVICES.contains(Build.DEVICE.toUpperCase());
- }
-
- /**
- * The mount that the crop rect needs to be scaled in x.
- */
- public float getCropRectScaleX() {
- if (SAMSUNG_A3_2017.equals(Build.DEVICE.toUpperCase())) {
- // For Samsung A3 2017, the symptom seems to be that the preview's FOV is always 1/3
- // wider than it's supposed to be. For example, if the preview Surface is 800x600, it's
- // actually has a FOV of 1066x600, but stretched to fit the 800x600 buffer. To correct
- // the preview, we need to crop out the extra 25% FOV.
- return 0.75f;
- }
- // No scale.
- return 1;
- }
-
- /**
- * The mount that the crop rect needs to be scaled in y.
- */
- public float getCropRectScaleY() {
- // No scale.
- return 1;
- }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java
new file mode 100644
index 0000000..46b49f5
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * A quirk where SurfaceView is stretched.
+ *
+ * <p> On Samsung Galaxy Z Fold2, transform APIs (e.g. View#setScaleX) do not work as intended.
+ * b/129403806
+ */
+public class SurfaceViewStretchedQuirk implements Quirk {
+
+ // Samsung Galaxy Z Fold2 b/129403806
+ private static final String SAMSUNG = "SAMSUNG";
+ private static final String GALAXY_Z_FOLD_2 = "F2Q";
+
+ static boolean load() {
+ return SAMSUNG.equals(Build.MANUFACTURER.toUpperCase()) && GALAXY_Z_FOLD_2.equals(
+ Build.DEVICE.toUpperCase());
+ }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java b/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
index d564532..6e4a804 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/video/ExperimentalVideo.java
@@ -18,7 +18,7 @@
import static java.lang.annotation.RetentionPolicy.CLASS;
-import androidx.annotation.experimental.Experimental;
+import androidx.annotation.RequiresOptIn;
import java.lang.annotation.Retention;
@@ -30,6 +30,6 @@
* releases, or may be removed altogether.
*/
@Retention(CLASS)
-@Experimental
+@RequiresOptIn
public @interface ExperimentalVideo {
}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
new file mode 100644
index 0000000..803a3a3
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.view.internal.compat.quirk.PreviewOneThirdWiderQuirk;
+import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/**
+ * Unit tests for {@link PreviewTransformation}.
+ */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class PreviewTransformationTest {
+
+ private static final Rect CROP_RECT = new Rect(0, 0, 600, 400);
+
+ private final PreviewTransformation mPreviewTransformation = new PreviewTransformation();
+
+ @Test
+ public void withPreviewStretchedQuirk_cropRectIsAdjusted() {
+ // Arrange.
+ QuirkInjector.inject(new PreviewOneThirdWiderQuirk());
+
+ // Act.
+ mPreviewTransformation.setTransformationInfo(
+ SurfaceRequest.TransformationInfo.of(CROP_RECT, 0, 0),
+ new Size(CROP_RECT.width(), CROP_RECT.height()),
+ /*isFrontCamera*/ false);
+
+ // Assert: the crop rect is corrected.
+ assertThat(mPreviewTransformation.getSurfaceCropRect()).isEqualTo(new Rect(75, 0, 525,
+ 400));
+ }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
new file mode 100644
index 0000000..82c2e25
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.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.camera.view;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.util.Size;
+
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.fakes.FakeCameraInfoInternal;
+import androidx.camera.view.internal.compat.quirk.QuirkInjector;
+import androidx.camera.view.internal.compat.quirk.SurfaceViewStretchedQuirk;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class PreviewViewTest {
+
+ @After
+ public void tearDown() {
+ QuirkInjector.clear();
+ }
+
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.N_MR1)
+ public void surfaceViewNormal_useSurfaceView() {
+ // Assert: SurfaceView is used.
+ assertThat(PreviewView.shouldUseTextureView(
+ createSurfaceRequestCompatibleWithSurfaceView(),
+ PreviewView.ImplementationMode.PERFORMANCE)).isFalse();
+ }
+
+ @Test
+ public void surfaceViewHasQuirk_useTextureView() {
+ // Arrange:
+ QuirkInjector.inject(new SurfaceViewStretchedQuirk());
+
+ // Assert: TextureView is used even the SurfaceRequest is compatible with SurfaceView.
+ assertThat(PreviewView.shouldUseTextureView(
+ createSurfaceRequestCompatibleWithSurfaceView(),
+ PreviewView.ImplementationMode.PERFORMANCE)).isTrue();
+ }
+
+ private SurfaceRequest createSurfaceRequestCompatibleWithSurfaceView() {
+ FakeCameraInfoInternal cameraInfoInternal = new FakeCameraInfoInternal();
+ cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
+ return new SurfaceRequest(new Size(800, 600),
+ new FakeCamera(null, cameraInfoInternal),
+ /*isRGB8888Required*/ false);
+ }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
new file mode 100644
index 0000000..d9f5964
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/DeviceQuirks.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.List;
+
+/**
+ * Tests version of main/.../DeviceQuirks.java, which provides device specific quirks, used for
+ * device specific workarounds.
+ * <p>
+ * In main/.../DeviceQuirks, Device quirks are loaded the first time a device workaround is
+ * encountered, and remain in memory until the process is killed. When running tests, this means
+ * that the same device quirks are used for all the tests. This causes an issue when tests modify
+ * device properties (using Robolectric for instance). Instead of force-reloading the device
+ * quirks in every test that uses a device workaround, this class internally reloads the quirks
+ * every time a device workaround is needed.
+ */
+public class DeviceQuirks {
+
+ private DeviceQuirks() {
+ }
+
+ /**
+ * Retrieves a specific device {@link Quirk} instance given its type.
+ *
+ * @param quirkClass The type of device quirk to retrieve.
+ * @return A device {@link Quirk} instance of the provided type, or {@code null} if it isn't
+ * found.
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public static <T extends Quirk> T get(@NonNull final Class<T> quirkClass) {
+ final List<Quirk> quirks = DeviceQuirksLoader.loadQuirks();
+ quirks.addAll(QuirkInjector.INJECTED_QUIRKS);
+ for (final Quirk quirk : quirks) {
+ if (quirk.getClass() == quirkClass) {
+ return (T) quirk;
+ }
+ }
+ return null;
+ }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
similarity index 61%
copy from camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
copy to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
index 0b42944..17c02f5 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewOneThirdWiderQuirkTest.java
@@ -28,24 +28,36 @@
import org.robolectric.util.ReflectionHelpers;
/**
- * Unit tests for {@link PreviewStretchedQuirk}.
+ * Unit tests for {@link PreviewOneThirdWiderQuirk}.
*/
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewStretchedQuirkTest {
+public class PreviewOneThirdWiderQuirkTest {
@Test
public void quirkExistsOnSamsungA3() {
- // Arrange.
ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
+ assertPreviewShouldBeCroppedBy25Percent();
+ }
- // Act.
- final PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+ @Test
+ @Config(minSdk = Build.VERSION_CODES.O)
+ public void quirkExistsOnSamsungJ5PrimeApi26AndAbove() {
+ ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+ assertPreviewShouldBeCroppedBy25Percent();
+ }
- // Assert.
+ @Test
+ @Config(maxSdk = Build.VERSION_CODES.N_MR1)
+ public void quirkDoesNotExistOnSamsungJ5PrimeApi25AndBelow() {
+ ReflectionHelpers.setStaticField(Build.class, "DEVICE", "ON5XELTE");
+ assertThat(DeviceQuirks.get(PreviewOneThirdWiderQuirk.class)).isNull();
+ }
+
+ private void assertPreviewShouldBeCroppedBy25Percent() {
+ final PreviewOneThirdWiderQuirk quirk = DeviceQuirks.get(PreviewOneThirdWiderQuirk.class);
assertThat(quirk).isNotNull();
assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
- assertThat(quirk.getCropRectScaleY()).isEqualTo(1F);
}
}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java
new file mode 100644
index 0000000..d7568c3
--- /dev/null
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/QuirkInjector.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.internal.compat.quirk;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Inject quirks for unit tests.
+ *
+ * <p> Used with the test version of {@link DeviceQuirks} to test the behavior of quirks.
+ */
+public class QuirkInjector {
+
+ static final List<Quirk> INJECTED_QUIRKS = new ArrayList<>();
+
+ /**
+ * Inject a quirk. The injected quirk will be loaded by {@link DeviceQuirks}.
+ */
+ public static void inject(@NonNull Quirk quirk) {
+ INJECTED_QUIRKS.add(quirk);
+ }
+
+ /**
+ * Clears all injected quirks.
+ */
+ public static void clear() {
+ INJECTED_QUIRKS.clear();
+ }
+}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
similarity index 78%
rename from camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
rename to camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
index 0b42944..965357e 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/PreviewStretchedQuirkTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/internal/compat/quirk/SurfaceViewStretchedQuirkTest.java
@@ -28,24 +28,23 @@
import org.robolectric.util.ReflectionHelpers;
/**
- * Unit tests for {@link PreviewStretchedQuirk}.
+ * Unit test for {@link SurfaceViewStretchedQuirk}.
*/
@RunWith(RobolectricTestRunner.class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-public class PreviewStretchedQuirkTest {
+public class SurfaceViewStretchedQuirkTest {
@Test
- public void quirkExistsOnSamsungA3() {
+ public void quirkExistsOnSamsungGalaxyZFold2() {
// Arrange.
- ReflectionHelpers.setStaticField(Build.class, "DEVICE", "A3Y17LTE");
+ ReflectionHelpers.setStaticField(Build.class, "DEVICE", "F2Q");
+ ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", "SAMSUNG");
// Act.
- final PreviewStretchedQuirk quirk = DeviceQuirks.get(PreviewStretchedQuirk.class);
+ final SurfaceViewStretchedQuirk quirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class);
// Assert.
assertThat(quirk).isNotNull();
- assertThat(quirk.getCropRectScaleX()).isEqualTo(0.75F);
- assertThat(quirk.getCropRectScaleY()).isEqualTo(1F);
}
}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 382ec89..79ad758 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,6 +18,7 @@
import android.Manifest
import android.app.Instrumentation
import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CoreAppTestUtil
import androidx.lifecycle.Lifecycle.State.CREATED
@@ -35,12 +36,14 @@
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.After
+import org.junit.AfterClass
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
private const val HOME_TIMEOUT_MS = 3000L
private const val ROTATE_TIMEOUT_MS = 2000L
@@ -62,6 +65,14 @@
Manifest.permission.RECORD_AUDIO
)
+ companion object {
+ @AfterClass
+ @JvmStatic
+ fun shutdownCameraX() {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+ }
+
@Before
fun setup() {
Assume.assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
index 5887576..42dce52 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/InitializationTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.integration.core
import android.content.Context
+import androidx.camera.core.CameraX
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CoreAppTestUtil
import androidx.concurrent.futures.await
@@ -32,6 +33,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.After
+import org.junit.AfterClass
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
@@ -39,6 +41,7 @@
import org.junit.rules.TestRule
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(Parameterized::class)
@@ -70,6 +73,12 @@
TestConfig(orientation)
}
}
+
+ @AfterClass
+ @JvmStatic
+ fun shutdownCameraX() {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
}
private var providerResult: CameraXViewModel.CameraProviderResult? = null
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
index 050cd6f..b1b8a1b 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/NewActivityLifecycleTest.java
@@ -26,6 +26,7 @@
import android.content.Context;
import android.content.Intent;
+import androidx.camera.core.CameraX;
import androidx.camera.testing.CameraUtil;
import androidx.camera.testing.CoreAppTestUtil;
import androidx.test.core.app.ApplicationProvider;
@@ -38,12 +39,17 @@
import androidx.test.uiautomator.UiDevice;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
// Test new activity lifecycle when using CameraX.
@RunWith(AndroidJUnit4.class)
@LargeTest
@@ -96,6 +102,12 @@
pressHomeButton();
}
+ @AfterClass
+ public static void shutdownCameraX()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS);
+ }
+
@Test
public void checkPreviewUpdatedWithNewInstance() {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
index 71cdc95..1c1704e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
@@ -17,6 +17,7 @@
package androidx.camera.integration.core
import android.Manifest
+import androidx.camera.core.CameraX
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CoreAppTestUtil
import androidx.test.core.app.ActivityScenario
@@ -27,12 +28,14 @@
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
+import org.junit.AfterClass
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
+import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(AndroidJUnit4::class)
@@ -47,6 +50,14 @@
Manifest.permission.RECORD_AUDIO
)
+ companion object {
+ @AfterClass
+ @JvmStatic
+ fun tearDown() {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+ }
+
@Before
fun setUp() {
assumeTrue(CameraUtil.deviceHasCamera())
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
index 2551bb3..70eccd1 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
@@ -34,6 +34,7 @@
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraX;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.TorchState;
import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
@@ -54,12 +55,17 @@
import junit.framework.AssertionFailedError;
import org.junit.After;
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
/** Test toggle buttons in CoreTestApp. */
@RunWith(AndroidJUnit4.class)
@LargeTest
@@ -118,6 +124,12 @@
mDevice.waitForIdle(IDLE_TIMEOUT_MS);
}
+ @AfterClass
+ public static void shutdownCameraX()
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS);
+ }
+
@Test
public void testFlashToggleButton() {
waitFor(new WaitForViewToShow(R.id.constraintLayout));
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
new file mode 100644
index 0000000..9cdff8a
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/UseCaseCombinationTest.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.core
+
+import android.content.Context
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.testing.CameraUtil
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import java.util.concurrent.TimeUnit
+
+private val DEFAULT_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
+
+/** Contains tests for [CameraX] which varies use case combinations to run. */
+@LargeTest
+@RunWith(Parameterized::class)
+class UseCaseCombinationTest(
+ private val implName: String,
+ private val cameraConfig: CameraXConfig
+) {
+
+ @get:Rule
+ val cameraRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+ arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+ )
+ }
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Before
+ fun initializeCameraX(): Unit = runBlocking {
+ CameraX.initialize(context, cameraConfig).get(10, TimeUnit.SECONDS)
+ }
+
+ @After
+ fun shutdownCameraX(): Unit = runBlocking {
+ CameraX.shutdown().get(10, TimeUnit.SECONDS)
+ }
+
+ /** Test Combination: Preview + ImageCapture */
+ @Test
+ fun previewCombinesImageCapture() = runBlocking {
+ val preview = initPreview()
+ val imageCapture = initImageCapture()
+
+ val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+ camera.detachUseCases()
+
+ // TODO(b/160249108) move off of main thread once UseCases can be attached on any thread
+ withContext(Dispatchers.Main) {
+ camera.addUseCases(listOf(preview, imageCapture))
+ }
+ }
+
+ /** Test Combination: Preview + ImageAnalysis */
+ @Test
+ fun previewCombinesImageAnalysis() = runBlocking {
+ val preview = initPreview()
+ val imageAnalysis = initImageAnalysis()
+
+ val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+ camera.detachUseCases()
+
+ // TODO(b/160249108) move off of main thread once UseCases can be attached on any thread
+ withContext(Dispatchers.Main) {
+ camera.addUseCases(listOf(preview, imageAnalysis))
+ }
+ }
+
+ /** Test Combination: Preview + ImageAnalysis + ImageCapture */
+ @Test
+ fun previewCombinesImageAnalysisAndImageCapture() = runBlocking {
+ val preview = initPreview()
+ val imageAnalysis = initImageAnalysis()
+ val imageCapture = initImageCapture()
+
+ val camera = CameraUtil.createCameraUseCaseAdapter(context, DEFAULT_SELECTOR)
+ camera.detachUseCases()
+
+ // TODO(b/160249108) move off of main thread once UseCases can be attached on any
+ // thread
+ withContext(Dispatchers.Main) {
+ camera.addUseCases(listOf(preview, imageAnalysis, imageCapture))
+ }
+ }
+
+ private fun initPreview(): Preview {
+ return Preview.Builder()
+ .setTargetName("Preview")
+ .build()
+ }
+
+ private fun initImageAnalysis(): ImageAnalysis {
+ return ImageAnalysis.Builder()
+ .setTargetName("ImageAnalysis")
+ .build()
+ }
+
+ private fun initImageCapture(): ImageCapture {
+ return ImageCapture.Builder().build()
+ }
+}
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 63937a2..2495047 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -56,6 +56,7 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.experimental.UseExperimental;
import androidx.appcompat.app.AppCompatActivity;
@@ -190,7 +191,7 @@
private FutureCallback<Integer> mEVFutureCallback = new FutureCallback<Integer>() {
@Override
- @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+ @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
public void onSuccess(@Nullable Integer result) {
CameraInfo cameraInfo = getCameraInfo();
if (cameraInfo != null) {
@@ -305,7 +306,7 @@
return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit();
}
- @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+ @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
private boolean isExposureCompensationSupported() {
CameraInfo cameraInfo = getCameraInfo();
return cameraInfo != null
@@ -445,7 +446,7 @@
});
}
- @UseExperimental(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
+ @OptIn(markerClass = androidx.camera.core.ExperimentalExposureCompensation.class)
private void setUpEVButton() {
mPlusEV.setOnClickListener(v -> {
Objects.requireNonNull(getCameraInfo());
@@ -824,7 +825,7 @@
* Workaround method for an AndroidX issue where {@link UseExperimental} doesn't support 2 or
* more annotations.
*/
- @UseExperimental(markerClass = ExperimentalUseCaseGroupLifecycle.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroupLifecycle.class)
private Camera bindToLifecycleSafely(List<UseCase> useCases) {
Log.e(TAG, "Binding use cases " + useCases);
return bindToLifecycleSafelyWithExperimental(useCases);
@@ -833,7 +834,7 @@
/**
* Binds use cases to the current lifecycle.
*/
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
@ExperimentalUseCaseGroupLifecycle
private Camera bindToLifecycleSafelyWithExperimental(List<UseCase> useCases) {
ViewPort viewPort = new ViewPort.Builder(new Rational(mViewFinder.getWidth(),
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
index 1c7deb2..af83fc9 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXViewModel.java
@@ -22,7 +22,7 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.pipe.integration.CameraPipeConfig;
import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration;
@@ -91,7 +91,7 @@
return mProcessCameraProviderLiveData;
}
- @UseExperimental(markerClass = ExperimentalCameraProviderConfiguration.class)
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
@MainThread
private static void tryConfigureCameraProvider() {
if (sConfiguredCameraXCameraImplementation == null) {
@@ -99,7 +99,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalCameraProviderConfiguration.class)
+ @OptIn(markerClass = ExperimentalCameraProviderConfiguration.class)
@MainThread
static void configureCameraProvider(@NonNull String cameraImplementation) {
if (!cameraImplementation.equals(sConfiguredCameraXCameraImplementation)) {
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
index 53242a6..50646a9 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/OpenGLRenderer.java
@@ -28,8 +28,8 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.WorkerThread;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.ExperimentalUseCaseGroup;
import androidx.camera.core.Preview;
import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -101,7 +101,7 @@
mExecutor.execute(() -> mNativeContext = initContext());
}
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
@MainThread
void attachInputPreview(@NonNull Preview preview) {
preview.setSurfaceProvider(
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
index 0aeb641..3823713 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/PreviewProcessorTimestampTest.java
@@ -33,7 +33,7 @@
import android.util.Size;
import androidx.annotation.NonNull;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.camera2.Camera2Config;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.CameraSelector;
@@ -136,7 +136,7 @@
}
@Before
- @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
public void setUp() throws Exception {
mProcessingHandlerThread =
new HandlerThread("Processing");
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
index 177210e..7c9077f 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoImageCaptureExtenderImpl.java
@@ -151,6 +151,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
index fbf70ba..0814b3d 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/AutoPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
@@ -96,6 +97,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
index 17b1eaa..e23aeb9 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyImageCaptureExtenderImpl.java
@@ -156,6 +156,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
index 0a8a654..4801055 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BeautyPreviewExtenderImpl.java
@@ -20,6 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
@@ -117,6 +118,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
index f2d82b1..eff6232 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehImageCaptureExtenderImpl.java
@@ -152,6 +152,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
index 9136f89..ca6a8da 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/BokehPreviewExtenderImpl.java
@@ -19,6 +19,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
import android.view.Surface;
@@ -135,6 +136,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
index f571dd9..564e941 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/CaptureProcessorImpl.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.impl;
+import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
import android.util.Pair;
@@ -45,9 +46,9 @@
* <p> The result of the processing step should be written to the {@link Surface} that was
* received by {@link #onOutputSurface(Surface, int)}.
*
- * @param results The map of images and metadata to process. The {@link Image} that are
- * contained within the map will become invalid after this method completes,
- * so no references to them should be kept.
+ * @param results The map of {@link ImageFormat#YUV_420_888} format images and metadata to
+ * process. The {@link Image} that are contained within the map will become
+ * invalid after this method completes, so no references to them should be kept.
*/
void process(Map<Integer, Pair<Image, TotalCaptureResult>> results);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
index f1817a7..010ce68 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/ExtenderStateListener.java
@@ -20,6 +20,7 @@
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.params.SessionConfiguration;
/**
* Provides interfaces that the OEM needs to implement to handle the state change.
@@ -51,8 +52,10 @@
* This will be invoked before creating a
* {@link android.hardware.camera2.CameraCaptureSession}. The {@link CaptureRequest}
* parameters returned via {@link CaptureStageImpl} will be passed to the camera device as
- * part of the capture session initialization via setSessionParameters(). The valid parameter
- * is a subset of the available capture request parameters.
+ * part of the capture session initialization via
+ * {@link SessionConfiguration#setSessionParameters(CaptureRequest)} which only supported
+ * from API level 28. The valid parameter is a subset of the available capture request
+ * parameters.
*
* @return The request information to set the session wide camera parameters.
*/
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
index 6ac0f3e..2ac4548 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/HdrImageCaptureExtenderImpl.java
@@ -229,6 +229,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
return captureStage;
}
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
index 7b0eb61..1434871 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightImageCaptureExtenderImpl.java
@@ -151,6 +151,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
index fb4a855..8bb6074 100755
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/NightPreviewExtenderImpl.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
import android.util.Pair;
import android.util.Size;
@@ -96,6 +97,12 @@
@Override
public CaptureStageImpl onPresetSession() {
+ // The CaptureRequest parameters will be set via SessionConfiguration#setSessionParameters
+ // (CaptureRequest) which only supported from API level 28.
+ if (Build.VERSION.SDK_INT < 28) {
+ return null;
+ }
+
// Set the necessary CaptureRequest parameters via CaptureStage, here we use some
// placeholder set of CaptureRequest.Key values
SettableCaptureStage captureStage = new SettableCaptureStage(SESSION_STAGE_ID);
diff --git a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
index 0e7879e..7caad1a 100644
--- a/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
+++ b/camera/integration-tests/extensionstestlib/src/main/java/androidx/camera/extensions/impl/PreviewImageProcessorImpl.java
@@ -16,6 +16,7 @@
package androidx.camera.extensions.impl;
+import android.graphics.ImageFormat;
import android.hardware.camera2.TotalCaptureResult;
import android.media.Image;
@@ -32,8 +33,8 @@
* <p> The result of the processing step should be written to the {@link android.view.Surface}
* that was received by {@link ProcessorImpl#onOutputSurface(android.view.Surface, int)}.
*
- * @param image The image to process. This will be invalid after the method completes so no
- * reference to it should be kept.
+ * @param image The {@link ImageFormat#YUV_420_888} format image to process. This will be
+ * invalid after the method completes so no reference to it should be kept.
* @param result The metadata associated with the image to process.
*/
void process(Image image, TotalCaptureResult result);
diff --git a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
index da988fa..8833509 100644
--- a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
@@ -34,8 +34,10 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
-
+ <!-- The attribute, requestLegacyExternalStorage, is ignored if app is targeted Android 11
+ (API level 30),. -->
<application
+ android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
index 19f829c..ab9f1a2 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
@@ -35,6 +35,7 @@
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.integration.antelope.MainActivity.Companion.PHOTOS_DIR
+import androidx.camera.integration.antelope.MainActivity.Companion.PHOTOS_PATH
import androidx.camera.integration.antelope.MainActivity.Companion.logd
import androidx.camera.integration.antelope.cameracontrollers.CameraState
import androidx.camera.integration.antelope.cameracontrollers.closeCameraX
@@ -186,7 +187,7 @@
* Actually write a byteArray file to disk. Assume the file is a jpg and use that extension
*/
fun writeFile(activity: MainActivity, bytes: ByteArray) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
writeFileAfterQ(activity, bytes)
} else {
writeFileBeforeQ(activity, bytes)
@@ -194,8 +195,10 @@
}
/**
- * Original writeFile implementation. It is workable on Pie and Pei lower for
- * Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
*/
fun writeFileBeforeQ(activity: MainActivity, bytes: ByteArray) {
val jpgFile = File(
@@ -255,21 +258,18 @@
}
/**
- * After Q, change to use MediaStore to access the shared media files.
+ * R and R above, change to use MediaStore to access the shared media files. Ref:
* https://developer.android.com/training/data-storage/shared
*/
fun writeFileAfterQ(activity: MainActivity, bytes: ByteArray) {
- var imageUri: Uri?
val resolver: ContentResolver = activity.contentResolver
-
- val relativeLocation = Environment.DIRECTORY_DCIM + File.separatorChar + PHOTOS_DIR
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, generateTimestamp().toString() + ".jpg")
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
- put(MediaStore.MediaColumns.RELATIVE_PATH, relativeLocation)
+ put(MediaStore.MediaColumns.RELATIVE_PATH, PHOTOS_PATH)
}
- imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+ val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
if (imageUri != null) {
val output = activity.contentResolver.openOutputStream(imageUri)
try {
@@ -303,9 +303,28 @@
}
/**
- * Delete all the photos generated by testing from the default Antelope PHOTOS_DIR
+ * Delete all the photos generated by testing
*/
fun deleteTestPhotos(activity: MainActivity) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ deleteTestPhotosAfterQ(activity)
+ } else {
+ deleteTestPhotosBeforeQ(activity)
+ }
+
+ activity.runOnUiThread {
+ Toast.makeText(activity, "All test photos deleted", Toast.LENGTH_SHORT).show()
+ }
+ logd("All photos in storage directory DCIM/" + PHOTOS_DIR + " deleted.")
+}
+
+/**
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
+ */
+fun deleteTestPhotosBeforeQ(activity: MainActivity) {
val photosDir = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
PHOTOS_DIR
@@ -320,15 +339,27 @@
val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
scannerIntent.data = Uri.fromFile(photosDir)
activity.sendBroadcast(scannerIntent)
-
- activity.runOnUiThread {
- Toast.makeText(activity, "All test photos deleted", Toast.LENGTH_SHORT).show()
- }
- logd("All photos in storage directory DCIM/" + PHOTOS_DIR + " deleted.")
}
}
/**
+ * R and R above, change to use MediaStore to delete the photo files. Ref:
+ * https://developer.android.com/training/data-storage/shared
+ */
+fun deleteTestPhotosAfterQ(activity: MainActivity) {
+ val imageDirUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
+ val resolver: ContentResolver = activity.contentResolver
+ val selection = MediaStore.MediaColumns.RELATIVE_PATH + " like ?"
+ val selectionArgs = arrayOf("%$PHOTOS_PATH%")
+
+ resolver.delete(
+ imageDirUri,
+ selection,
+ selectionArgs
+ )
+}
+
+/**
* Try to detect if a saved image file has had HDR effects applied to it by examining the EXIF tag.
*
* Note: this does not currently work.
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
index 2027e63..70a7ae4 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
@@ -23,6 +23,7 @@
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
+import android.os.Environment
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
@@ -42,6 +43,7 @@
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.test.espresso.idling.CountingIdlingResource
+import java.io.File
/**
* Main Antelope Activity
@@ -93,6 +95,8 @@
/** Idling Resource used for Espresso tests */
public val antelopeIdlingResource = CountingIdlingResource("AntelopeIdlingResource")
+ val PHOTOS_PATH = Environment.DIRECTORY_DCIM + File.separatorChar + PHOTOS_DIR
+ val LOG_PATH = Environment.DIRECTORY_DOCUMENTS + File.separatorChar + LOG_DIR
/** Convenience wrapper for Log.d that can be toggled on/off */
fun logd(message: String) {
if (camViewModel.getShouldOutputLog().value ?: false)
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
index 5dedacf..03eaa24 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
@@ -26,7 +26,7 @@
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
-import androidx.camera.integration.antelope.MainActivity.Companion.LOG_DIR
+import androidx.camera.integration.antelope.MainActivity.Companion.LOG_PATH
import androidx.camera.integration.antelope.MainActivity.Companion.logd
import com.google.common.math.Quantiles
import com.google.common.math.Stats
@@ -281,7 +281,7 @@
* @param csv The comma-based csv string
*/
fun writeCSV(activity: MainActivity, filePrefix: String, csv: String) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
writeCSVAfterQ(activity, filePrefix, csv)
} else {
writeCSVBeforeQ(activity, filePrefix, csv)
@@ -289,8 +289,10 @@
}
/**
-* Original writeFile implementation. It is workable on Pie and Pei lower for
-* Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
*/
fun writeCSVBeforeQ(activity: MainActivity, prefix: String, csv: String) {
val csvFile = File(
@@ -374,7 +376,7 @@
}
/**
- * After Q, change to use MediaStore to access the shared media files.
+ * R and R above, change to use MediaStore to access the shared media files.
* https://developer.android.com/training/data-storage/shared
*
* @param activity The main activity
@@ -383,17 +385,17 @@
*/
fun writeCSVAfterQ(activity: MainActivity, prefix: String, csv: String) {
var output: OutputStream?
- var csvUri: Uri?
val resolver: ContentResolver = activity.contentResolver
-
- val relativePath = Environment.DIRECTORY_DOCUMENTS + File.separatorChar + LOG_DIR
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, prefix + "_" + generateCSVTimestamp() + ".csv")
put(MediaStore.MediaColumns.MIME_TYPE, "text/comma-separated-values")
- put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+ put(MediaStore.MediaColumns.RELATIVE_PATH, LOG_PATH)
}
- csvUri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
+ val csvUri = resolver.insert(
+ MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
+ contentValues
+ )
if (csvUri != null) {
lateinit var bufferWriter: BufferedWriter
try {
@@ -422,9 +424,45 @@
}
/**
- * Delete all Antelope .csv files in the documents directory
+ * Delete all Antelope .csv files in the Documents directory
*/
fun deleteCSVFiles(activity: MainActivity) {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ deleteCSVFilesAfterQ(activity)
+ } else {
+ deleteCSVFilesBeforeQ(activity)
+ }
+
+ activity.runOnUiThread {
+ Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
+ }
+ logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
+}
+
+/**
+ * R and R above, change to use MediaStore to delete the log files. It will delete records in media
+ * store and the physical log files.
+ */
+fun deleteCSVFilesAfterQ(activity: MainActivity) {
+ val logDirUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
+ val resolver: ContentResolver = activity.contentResolver
+ val selection = MediaStore.MediaColumns.RELATIVE_PATH + " like ?"
+ val selectionArgs = arrayOf("%$LOG_PATH%")
+
+ resolver.delete(
+ logDirUri,
+ selection,
+ selectionArgs
+ )
+}
+
+/**
+ * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
+ * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
+ * make it workable. Ref:
+ * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
+ */
+fun deleteCSVFilesBeforeQ(activity: MainActivity) {
val csvDir = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS
@@ -441,11 +479,6 @@
val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
scannerIntent.data = Uri.fromFile(csvDir)
activity.sendBroadcast(scannerIntent)
-
- activity.runOnUiThread {
- Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
- }
- logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
}
}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
index e147a94..f86966c 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
@@ -22,7 +22,7 @@
import android.util.Log
import android.view.Surface
import android.view.ViewGroup
-import androidx.annotation.experimental.UseExperimental
+import androidx.annotation.OptIn
import androidx.camera.camera2.interop.Camera2Interop
import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.core.CameraSelector
@@ -307,7 +307,7 @@
/**
* Setup the Camera X preview use case
*/
-@UseExperimental(markerClass = ExperimentalCamera2Interop::class)
+@OptIn(ExperimentalCamera2Interop::class)
private fun cameraXPreviewUseCaseBuilder(
focusMode: FocusMode,
deviceStateCallback: CameraDevice.StateCallback,
@@ -328,7 +328,7 @@
/**
* Setup the Camera X image capture use case
*/
-@UseExperimental(markerClass = ExperimentalCamera2Interop::class)
+@OptIn(ExperimentalCamera2Interop::class)
private fun cameraXImageCaptureUseCaseBuilder(
focusMode: FocusMode,
deviceStateCallback:
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e833abd..46cb4308 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -37,9 +37,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
@@ -117,7 +117,7 @@
@NonNull
@Override
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@@ -367,7 +367,7 @@
/**
* Updates UI text based on the state of {@link #mCameraController}.
*/
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private void updateUiText() {
mFlashMode.setText(getFlashModeTextResId());
final Integer lensFacing = mCameraController.getCameraSelector().getLensFacing();
@@ -424,7 +424,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private void onUseCaseToggled(CompoundButton compoundButton, boolean value) {
if (mCaptureEnabledToggle == null || mAnalysisEnabledToggle == null
|| mVideoEnabledToggle == null) {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
index d21b2f5..4cf40a6 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraViewFragment.java
@@ -30,7 +30,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.core.CameraSelector;
import androidx.camera.view.CameraView;
import androidx.camera.view.CameraView.CaptureMode;
@@ -152,7 +152,7 @@
}
@Override
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
@@ -261,7 +261,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
void updateModeButtonIcon() {
if (mCameraView.getCaptureMode() == CaptureMode.MIXED) {
mModeButton.setButtonDrawable(R.drawable.ic_photo_camera);
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
index c109dfd..0f08848 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CaptureViewOnTouchListener.java
@@ -31,7 +31,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.experimental.UseExperimental;
+import androidx.annotation.OptIn;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
import androidx.camera.core.ImageCaptureException;
@@ -101,7 +101,7 @@
}
/** Called when the user taps. */
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private void onTap() {
if (mCameraView.getCaptureMode() == CaptureMode.IMAGE
|| mCameraView.getCaptureMode() == CaptureMode.MIXED) {
@@ -142,7 +142,7 @@
}
/** Called when the user holds (long presses). */
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private void onHold() {
if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
|| mCameraView.getCaptureMode() == CaptureMode.MIXED) {
@@ -167,7 +167,7 @@
}
/** Called when the user releases. */
- @UseExperimental(markerClass = ExperimentalVideo.class)
+ @OptIn(markerClass = ExperimentalVideo.class)
private void onRelease() {
if (mCameraView.getCaptureMode() == CaptureMode.VIDEO
|| mCameraView.getCaptureMode() == CaptureMode.MIXED) {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
index 6933449..fce0f3e 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/PreviewViewFragment.java
@@ -37,8 +37,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
import androidx.annotation.VisibleForTesting;
-import androidx.annotation.experimental.UseExperimental;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
@@ -166,7 +166,7 @@
}
}
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
void setUpTargetRotationButton(@NonNull final ProcessCameraProvider cameraProvider,
@NonNull final View rootView) {
Button button = rootView.findViewById(R.id.target_rotation);
@@ -328,8 +328,8 @@
}
@SuppressWarnings("WeakerAccess")
- @UseExperimental(markerClass = ExperimentalUseCaseGroup.class)
- @SuppressLint("UnsafeExperimentalUsageError")
+ @OptIn(markerClass = ExperimentalUseCaseGroup.class)
+ @SuppressLint("UnsafeOptInUsageError")
void bindPreview(@NonNull ProcessCameraProvider cameraProvider) {
if (mPreview == null) {
return;
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
index d15867e..5b18e9d 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/TransformFragment.java
@@ -161,9 +161,13 @@
// Loop through the y plane and get the sum of the luminance for each tile.
byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
image.getPlanes()[0].getBuffer().get(bytes);
+ int tileX;
+ int tileY;
for (int x = 0; x < cropRect.width(); x++) {
for (int y = 0; y < cropRect.height(); y++) {
- tiles[x / tileWidth][y / tileHeight] +=
+ tileX = Math.min(x / tileWidth, TILE_COUNT - 1);
+ tileY = Math.min(y / tileHeight, TILE_COUNT - 1);
+ tiles[tileX][tileY] +=
bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
}
}
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
index ee0b902..7938a58 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/fragment_main.xml
@@ -56,8 +56,8 @@
style="?android:buttonBarButtonStyle"
android:layout_width="match_parent"
android:layout_height="0dp"
- android:layout_marginBottom="50dp"
- android:layout_marginTop="50dp"
+ android:layout_marginBottom="10dp"
+ android:layout_marginTop="10dp"
android:layout_weight="1"
android:text="@string/btn_capture" />
diff --git a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
index aebdc61..a8791e7 100644
--- a/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
+++ b/car/app/app-activity/src/main/java/androidx/car/app/activity/CarAppActivity.java
@@ -335,7 +335,8 @@
//TODO("b/177083268: Multiple hosts support is not implemented")
}
- if (!bindService(rendererIntent, mServiceConnectionImpl, Context.BIND_AUTO_CREATE)) {
+ if (!bindService(rendererIntent, mServiceConnectionImpl,
+ Context.BIND_AUTO_CREATE | Context.BIND_INCLUDE_CAPABILITIES)) {
onServiceConnectionError(
"Cannot bind to the renderer host with intent: " + rendererIntent);
}
@@ -402,7 +403,6 @@
} catch (RemoteException e) {
// We are already unbinding (maybe because the host has already cut the connection)
// Let's not log more errors unnecessarily.
- //TODO(179506019): Revisit calls to unbindService()
}
unbindService(mServiceConnectionImpl);
diff --git a/car/app/app-samples/helloworld/build.gradle b/car/app/app-samples/helloworld/build.gradle
index a6b8625..8cb3f37 100644
--- a/car/app/app-samples/helloworld/build.gradle
+++ b/car/app/app-samples/helloworld/build.gradle
@@ -1,7 +1,4 @@
-import static androidx.build.dependencies.DependenciesKt.ANDROIDX_TEST_CORE
-import static androidx.build.dependencies.DependenciesKt.JUNIT
-import static androidx.build.dependencies.DependenciesKt.ROBOLECTRIC
-import static androidx.build.dependencies.DependenciesKt.TRUTH
+import static androidx.build.dependencies.DependenciesKt.*
/*
* Copyright (C) 2021 The Android Open Source Project
@@ -26,7 +23,7 @@
android {
defaultConfig {
- applicationId "androidx.car.app.samples.helloworld"
+ applicationId "androidx.car.app.sample.helloworld"
minSdkVersion 23
versionCode 1
versionName "1.0"
diff --git a/car/app/app-samples/helloworld/github_build.gradle b/car/app/app-samples/helloworld/github_build.gradle
index b6e2004..feb0b85a 100644
--- a/car/app/app-samples/helloworld/github_build.gradle
+++ b/car/app/app-samples/helloworld/github_build.gradle
@@ -20,7 +20,7 @@
compileSdkVersion 29
defaultConfig {
- applicationId "androidx.car.app.samples.helloworld"
+ applicationId "androidx.car.app.sample.helloworld"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
@@ -34,7 +34,7 @@
}
dependencies {
- implementation "androidx.car.app:app:1.0.0-beta01"
+ implementation "androidx.car.app:app:1.0.0-rc01"
}
diff --git a/car/app/app-samples/helloworld/src/main/AndroidManifest.xml b/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
index c06779c..57f9e09 100644
--- a/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/helloworld/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="androidx.car.app.samples.helloworld"
+ package="androidx.car.app.sample.helloworld"
android:versionCode="1"
android:versionName="1.0">
<application
@@ -26,7 +26,7 @@
android:resource="@xml/automotive_app_desc"
tools:ignore="MetadataTagInsideApplicationTag" />
<service
- android:name=".HelloWorldService"
+ android:name="androidx.car.app.sample.helloworld.HelloWorldService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
diff --git a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
similarity index 96%
rename from car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java
rename to car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
index d1602f5..048e20a 100644
--- a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldScreen.java
+++ b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldScreen.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java
rename to car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
index 6e71d87..d333abf 100644
--- a/car/app/app-samples/helloworld/src/main/java/androidx/car/app/samples/helloworld/HelloWorldService.java
+++ b/car/app/app-samples/helloworld/src/main/java/androidx/car/app/sample/helloworld/HelloWorldService.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
diff --git a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java
rename to car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
index 0d96725..ec49b05 100644
--- a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldScreenTest.java
+++ b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldScreenTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
import static com.google.common.truth.Truth.assertThat;
diff --git a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
similarity index 97%
rename from car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java
rename to car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
index 1a126b0..e510ecb 100644
--- a/car/app/app-samples/helloworld/src/test/java/androidx/car/app/samples/helloworld/HelloWorldSessionTest.java
+++ b/car/app/app-samples/helloworld/src/test/java/androidx/car/app/sample/helloworld/HelloWorldSessionTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.helloworld;
+package androidx.car.app.sample.helloworld;
import static com.google.common.truth.Truth.assertThat;
diff --git a/car/app/app-samples/navigation/build.gradle b/car/app/app-samples/navigation/build.gradle
index 8365d1e..ce1db708 100644
--- a/car/app/app-samples/navigation/build.gradle
+++ b/car/app/app-samples/navigation/build.gradle
@@ -21,7 +21,7 @@
android {
defaultConfig {
- applicationId "androidx.car.app.samples.navigation"
+ applicationId "androidx.car.app.sample.navigation"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
diff --git a/car/app/app-samples/navigation/github_build.gradle b/car/app/app-samples/navigation/github_build.gradle
index 41ece82..1ebb356 100644
--- a/car/app/app-samples/navigation/github_build.gradle
+++ b/car/app/app-samples/navigation/github_build.gradle
@@ -20,7 +20,7 @@
compileSdkVersion 29
defaultConfig {
- applicationId "androidx.car.app.samples.navigation"
+ applicationId "androidx.car.app.sample.navigation"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
@@ -37,5 +37,5 @@
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
implementation "androidx.core:core:1.5.0-alpha01"
- implementation "androidx.car.app:app:1.0.0-beta01"
+ implementation "androidx.car.app:app:1.0.0-rc01"
}
diff --git a/car/app/app-samples/navigation/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/src/main/AndroidManifest.xml
index d9457f7..9d1e3f4 100644
--- a/car/app/app-samples/navigation/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="androidx.car.app.samples.navigation"
+ package="androidx.car.app.sample.navigation"
android:versionCode="1"
android:versionName="1.0">
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
index 4abbdc3..d4a117c 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/app/MainActivity.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/app/MainActivity.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.app;
+package androidx.car.app.sample.navigation.app;
import android.Manifest;
import android.app.Activity;
@@ -29,8 +29,8 @@
import android.widget.Button;
import androidx.annotation.NonNull;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.nav.NavigationService;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.nav.NavigationService;
/**
* The main app activity.
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
index 5a84ab5..ee43bb0 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/FavoritesScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/FavoritesScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
@@ -36,9 +36,9 @@
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.DemoScripts;
-import androidx.car.app.samples.navigation.model.PlaceInfo;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.PlaceInfo;
import java.util.ArrayList;
import java.util.List;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
index 9cdf56f..6c36afb 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationCarAppService.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationCarAppService.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import android.annotation.SuppressLint;
import android.app.Notification;
@@ -27,7 +27,7 @@
import androidx.annotation.NonNull;
import androidx.car.app.CarAppService;
import androidx.car.app.Session;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
import androidx.car.app.validation.HostValidator;
import androidx.core.app.NotificationCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
similarity index 98%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
index 0781c43..9779ffe 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -33,8 +33,8 @@
import androidx.car.app.navigation.model.RoutingInfo;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.Instruction;
import androidx.core.graphics.drawable.IconCompat;
import java.util.ArrayList;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
similarity index 93%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
index cc41007..713ae63 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/NavigationSession.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/NavigationSession.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import android.Manifest;
import android.annotation.SuppressLint;
@@ -28,6 +28,7 @@
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
+import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
@@ -44,10 +45,10 @@
import androidx.car.app.navigation.model.Destination;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.model.Instruction;
-import androidx.car.app.samples.navigation.nav.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.navigation.nav.NavigationService;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.nav.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.navigation.nav.NavigationService;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
@@ -112,6 +113,20 @@
public void onLocationChanged(Location location) {
mNavigationCarSurface.updateLocationString(getLocationString(location));
}
+
+ /** @deprecated This callback will never be invoked on Android Q and above. */
+ @Override
+ @Deprecated
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
};
// Monitors the state of the connection to the Navigation service.
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
index 626def0..962442b 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RequestPermissionScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RequestPermissionScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -33,7 +33,7 @@
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.ParkedOnlyOnClickListener;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.navigation.app.MainActivity;
+import androidx.car.app.sample.navigation.app.MainActivity;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
index 94a2472..d35bddd 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/RoutePreviewScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/RoutePreviewScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import android.text.SpannableString;
import android.util.Log;
@@ -29,7 +29,7 @@
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.RoutePreviewNavigationTemplate;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
import java.util.ArrayList;
import java.util.List;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
similarity index 96%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
index 7b27774..53dd243 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchResultsScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchResultsScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
@@ -35,8 +35,8 @@
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.navigation.model.DemoScripts;
-import androidx.car.app.samples.navigation.model.PlaceInfo;
+import androidx.car.app.sample.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.PlaceInfo;
/** Screen for showing a list of places from a search. */
public final class SearchResultsScreen extends Screen {
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
index a65e8be..6a45232 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SearchScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SearchScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -26,7 +26,7 @@
import androidx.car.app.model.SearchTemplate;
import androidx.car.app.model.SearchTemplate.SearchCallback;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.navigation.model.DemoScripts;
+import androidx.car.app.sample.navigation.model.DemoScripts;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
index 559e76e..143ccd8 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SettingsScreen.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SettingsScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import android.content.Context;
import android.content.SharedPreferences;
@@ -29,7 +29,7 @@
import androidx.car.app.model.SectionedItemList;
import androidx.car.app.model.Template;
import androidx.car.app.model.Toggle;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
/** Settings screen demo. */
public final class SettingsScreen extends Screen {
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
similarity index 99%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
index 3b373d8..c148881 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/car/SurfaceRenderer.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/car/SurfaceRenderer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.car;
+package androidx.car.app.sample.navigation.car;
import android.graphics.Canvas;
import android.graphics.Color;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
similarity index 98%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
index 97c4aa0..d7bf70a 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/DemoScripts.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/DemoScripts.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_RIGHT;
import static androidx.car.app.navigation.model.LaneDirection.SHAPE_STRAIGHT;
@@ -79,7 +79,7 @@
import androidx.car.app.navigation.model.Maneuver;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.navigation.R;
+import androidx.car.app.sample.navigation.R;
import androidx.core.graphics.drawable.IconCompat;
import java.util.ArrayList;
@@ -140,13 +140,7 @@
new CarIcon.Builder(
IconCompat.createWithResource(
carContext,
- androidx.car
- .app
- .samples
- .navigation
- .R
- .drawable
- .junction_image))
+ R.drawable.junction_image))
.build();
Lane straightNormal =
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
similarity index 99%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
index 56f688f..7ddb4ca 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Instruction.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Instruction.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
similarity index 95%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
index 05a0d8f..237325e 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/PlaceInfo.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/PlaceInfo.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
import androidx.annotation.NonNull;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
index a8ff7e84e..baf9976 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/model/Script.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/model/Script.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.model;
+package androidx.car.app.sample.navigation.model;
import android.os.Handler;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
similarity index 94%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
index 67b55a9..13c0758 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/DeepLinkNotificationReceiver.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/DeepLinkNotificationReceiver.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.nav;
+package androidx.car.app.sample.navigation.nav;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
@@ -23,7 +23,7 @@
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
-import androidx.car.app.samples.navigation.car.NavigationCarAppService;
+import androidx.car.app.sample.navigation.car.NavigationCarAppService;
import java.util.Arrays;
import java.util.HashSet;
diff --git a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
similarity index 97%
rename from car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
rename to car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
index 9d07221..41a5f5ef 100644
--- a/car/app/app-samples/navigation/src/main/java/androidx/car/app/samples/navigation/nav/NavigationService.java
+++ b/car/app/app-samples/navigation/src/main/java/androidx/car/app/sample/navigation/nav/NavigationService.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package androidx.car.app.samples.navigation.nav;
+package androidx.car.app.sample.navigation.nav;
import static android.media.AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
-import static androidx.car.app.samples.navigation.nav.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
+import static androidx.car.app.sample.navigation.nav.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
import android.annotation.SuppressLint;
import android.app.Notification;
@@ -54,10 +54,10 @@
import androidx.car.app.navigation.model.TravelEstimate;
import androidx.car.app.navigation.model.Trip;
import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.navigation.R;
-import androidx.car.app.samples.navigation.app.MainActivity;
-import androidx.car.app.samples.navigation.model.Instruction;
-import androidx.car.app.samples.navigation.model.Script;
+import androidx.car.app.sample.navigation.R;
+import androidx.car.app.sample.navigation.app.MainActivity;
+import androidx.car.app.sample.navigation.model.Instruction;
+import androidx.car.app.sample.navigation.model.Script;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
@@ -79,7 +79,7 @@
// Constants for location broadcast
private static final String PACKAGE_NAME =
- "androidx.car.app.samples.navigation.nav.navigationservice";
+ "androidx.car.app.sample.navigation.nav.navigationservice";
private static final String EXTRA_STARTED_FROM_NOTIFICATION =
PACKAGE_NAME + ".started_from_notification";
diff --git a/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml b/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
index b311504..e84efcf 100644
--- a/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
+++ b/car/app/app-samples/navigation/src/main/res/layout/activity_main.xml
@@ -15,9 +15,7 @@
limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
diff --git a/car/app/app-samples/places/build.gradle b/car/app/app-samples/places/build.gradle
index 01222f5..bc3b2e7 100644
--- a/car/app/app-samples/places/build.gradle
+++ b/car/app/app-samples/places/build.gradle
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
+
+import static androidx.build.dependencies.DependenciesKt.GUAVA_ANDROID
plugins {
id("AndroidXPlugin")
@@ -22,7 +23,7 @@
android {
defaultConfig {
- applicationId "androidx.car.app.samples.places"
+ applicationId "androidx.car.app.sample.places"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
diff --git a/car/app/app-samples/places/github_build.gradle b/car/app/app-samples/places/github_build.gradle
index 681d3c18..7d94154 100644
--- a/car/app/app-samples/places/github_build.gradle
+++ b/car/app/app-samples/places/github_build.gradle
@@ -19,7 +19,7 @@
android {
compileSdkVersion 29
defaultConfig {
- applicationId "androidx.car.app.samples.places"
+ applicationId "androidx.car.app.sample.places"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
@@ -36,5 +36,5 @@
implementation 'com.github.bumptech.glide:glide:4.9.0'
implementation 'com.google.guava:guava:28.1-jre'
- implementation "androidx.car.app:app:1.0.0-beta01"
+ implementation "androidx.car.app:app:1.0.0-rc01"
}
\ No newline at end of file
diff --git a/car/app/app-samples/places/src/main/AndroidManifest.xml b/car/app/app-samples/places/src/main/AndroidManifest.xml
index 3e6b254..01fe5dd 100644
--- a/car/app/app-samples/places/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/places/src/main/AndroidManifest.xml
@@ -16,7 +16,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="androidx.car.app.samples.places"
+ package="androidx.car.app.sample.places"
android:versionCode="1"
android:versionName="1.0">
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
index 00bdf75..f27d43a 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Constants.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Constants.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import android.location.Location;
-import androidx.car.app.samples.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceCategory;
/** App-wide constants */
class Constants {
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
index b33177e..9e672d3 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/Executors.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/Executors.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
index e838eaa..2bb5cc9 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/ImageUtil.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/ImageUtil.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import android.content.Context;
import android.graphics.Bitmap;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
index ee2c806..ab1ad5e 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceCategoryListScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceCategoryListScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import android.location.Location;
@@ -32,7 +32,7 @@
import androidx.car.app.model.PlaceMarker;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceCategory;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
index 40d20ef..44abe3b 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceDetailsScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceDetailsScreen.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
import android.content.Context;
import android.content.Intent;
@@ -40,9 +40,9 @@
import androidx.car.app.model.PaneTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceDetails;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceDetails;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
similarity index 95%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
index 8bebc62..9440a31 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlaceListScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlaceListScreen.java
@@ -14,12 +14,12 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE;
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
import android.location.Geocoder;
import android.location.Location;
@@ -41,9 +41,9 @@
import androidx.car.app.model.PlaceMarker;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceCategory;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceCategory;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
index 7c80ab2..de90086 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/PlacesCarAppService.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/PlacesCarAppService.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
similarity index 94%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
index 94829d9..51e427d 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/SearchScreen.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/SearchScreen.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
-import static androidx.car.app.samples.places.Executors.BACKGROUND_EXECUTOR;
-import static androidx.car.app.samples.places.Executors.UI_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.BACKGROUND_EXECUTOR;
+import static androidx.car.app.sample.places.Executors.UI_EXECUTOR;
import android.location.Geocoder;
import android.location.Location;
@@ -31,8 +31,8 @@
import androidx.car.app.model.SearchTemplate;
import androidx.car.app.model.SearchTemplate.SearchCallback;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.places.places.PlaceFinder;
-import androidx.car.app.samples.places.places.PlaceInfo;
+import androidx.car.app.sample.places.places.PlaceFinder;
+import androidx.car.app.sample.places.places.PlaceInfo;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
similarity index 98%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
index c539ffd..cd0c8da 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/UiExecutor.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/UiExecutor.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places;
+package androidx.car.app.sample.places;
import android.os.Handler;
import android.os.Looper;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
similarity index 96%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
index 401aa73..0f376e7 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/LocationUtil.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/LocationUtil.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
import android.location.Address;
import android.location.Geocoder;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
similarity index 96%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
index c3ae62e..2c99e25 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceCategory.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceCategory.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
import androidx.annotation.NonNull;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
index 324cc5c..d2c6f5e 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceDetails.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceDetails.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
similarity index 99%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
index 4b4ea2a..70ff88a 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceFinder.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceFinder.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
import android.location.Location;
import android.util.Log;
diff --git a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
similarity index 97%
rename from car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java
rename to car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
index 3f01357..6f92a08 100644
--- a/car/app/app-samples/places/src/main/java/androidx/car/app/samples/places/places/PlaceInfo.java
+++ b/car/app/app-samples/places/src/main/java/androidx/car/app/sample/places/places/PlaceInfo.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.places.places;
+package androidx.car.app.sample.places.places;
import android.location.Address;
import android.location.Geocoder;
diff --git a/car/app/app-samples/places/src/main/res/values/themes.xml b/car/app/app-samples/places/src/main/res/values/themes.xml
index 1f311ba..10c8630 100644
--- a/car/app/app-samples/places/src/main/res/values/themes.xml
+++ b/car/app/app-samples/places/src/main/res/values/themes.xml
@@ -16,8 +16,7 @@
-->
<resources>
- <style name="CarAppTheme"
- xmlns:custom="http://schemas.android.com/apk/res-auto">
+ <style name="CarAppTheme">
<item name="carColorPrimary">#ff41af6a</item>
<item name="carColorPrimaryDark">#ff41af6a</item>
<item name="carColorSecondary">#fff3b713</item>
diff --git a/car/app/app-samples/showcase/build.gradle b/car/app/app-samples/showcase/build.gradle
index 661adad..04c40c5 100644
--- a/car/app/app-samples/showcase/build.gradle
+++ b/car/app/app-samples/showcase/build.gradle
@@ -21,7 +21,7 @@
android {
defaultConfig {
- applicationId "androidx.car.app.samples.showcase"
+ applicationId "androidx.car.app.sample.showcase"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
@@ -43,4 +43,5 @@
implementation(project(":car:app:app"))
implementation("androidx.core:core:1.5.0-alpha01")
+ implementation project(path: ':annotation:annotation-experimental')
}
diff --git a/car/app/app-samples/showcase/github_build.gradle b/car/app/app-samples/showcase/github_build.gradle
index 68f2c69..f07e0ae 100644
--- a/car/app/app-samples/showcase/github_build.gradle
+++ b/car/app/app-samples/showcase/github_build.gradle
@@ -20,7 +20,7 @@
compileSdkVersion 29
defaultConfig {
- applicationId "androidx.car.app.samples.showcase"
+ applicationId "androidx.car.app.sample.showcase"
minSdkVersion 23
targetSdkVersion 29
versionCode 1
@@ -38,5 +38,6 @@
dependencies {
implementation "androidx.core:core:1.5.0-alpha01"
- implementation "androidx.car.app:app:1.0.0-beta01"
+ implementation "androidx.car.app:app:1.0.0-rc01"
+ implementation "androidx.annotation:annotation-experimental:1.0.0"
}
diff --git a/car/app/app-samples/showcase/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
index 50f778d..9a8b0281 100644
--- a/car/app/app-samples/showcase/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/src/main/AndroidManifest.xml
@@ -17,7 +17,7 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
- package="androidx.car.app.samples.showcase"
+ package="androidx.car.app.sample.showcase"
android:versionCode="1"
android:versionName="1.0">
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
index a6f7663..f77f978 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/DeepLinkNotificationReceiver.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/DeepLinkNotificationReceiver.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
index d83979f..0904197 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/SelectableListsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/SelectableListsDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import static androidx.car.app.CarToast.LENGTH_LONG;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
index 146af0c..1abcb557d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseService.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseService.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import android.content.pm.ApplicationInfo;
import android.net.Uri;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
similarity index 93%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
index 1c4d160..0b47170 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/ShowcaseSession.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/ShowcaseSession.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -31,12 +31,12 @@
import androidx.car.app.Screen;
import androidx.car.app.ScreenManager;
import androidx.car.app.Session;
-import androidx.car.app.samples.showcase.misc.GoToPhoneScreen;
-import androidx.car.app.samples.showcase.misc.PreSeedingFlowScreen;
-import androidx.car.app.samples.showcase.misc.ReservationCancelledScreen;
-import androidx.car.app.samples.showcase.navigation.NavigationNotificationsDemoScreen;
-import androidx.car.app.samples.showcase.navigation.SurfaceRenderer;
-import androidx.car.app.samples.showcase.navigation.routing.NavigatingDemoScreen;
+import androidx.car.app.sample.showcase.misc.GoToPhoneScreen;
+import androidx.car.app.sample.showcase.misc.PreSeedingFlowScreen;
+import androidx.car.app.sample.showcase.misc.ReservationCancelledScreen;
+import androidx.car.app.sample.showcase.navigation.NavigationNotificationsDemoScreen;
+import androidx.car.app.sample.showcase.navigation.SurfaceRenderer;
+import androidx.car.app.sample.showcase.navigation.routing.NavigatingDemoScreen;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
similarity index 93%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
index 6b66d75..9ca77f1 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/StartScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/StartScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
@@ -25,10 +25,10 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.misc.MiscDemoScreen;
-import androidx.car.app.samples.showcase.navigation.NavigationDemosScreen;
-import androidx.car.app.samples.showcase.templates.MiscTemplateDemosScreen;
-import androidx.car.app.samples.showcase.textandicons.TextAndIconsDemosScreen;
+import androidx.car.app.sample.showcase.misc.MiscDemoScreen;
+import androidx.car.app.sample.showcase.navigation.NavigationDemosScreen;
+import androidx.car.app.sample.showcase.templates.MiscTemplateDemosScreen;
+import androidx.car.app.sample.showcase.textandicons.TextAndIconsDemosScreen;
import androidx.core.graphics.drawable.IconCompat;
/** The starting screen of the app. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
similarity index 99%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
index de416c6..7eca43c 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/TaskRestrictionDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/TaskRestrictionDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase;
+package androidx.car.app.sample.showcase;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
index a1fd9b2..3ea8e4f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceDetailsScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceDetailsScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
index 651177f..d3d5db8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/PlaceInfo.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/PlaceInfo.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
import android.location.Location;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
index 1503215..83ef219 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/SamplePlaces.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/SamplePlaces.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
import android.graphics.BitmapFactory;
import android.location.Location;
@@ -35,7 +35,7 @@
import androidx.car.app.model.Place;
import androidx.car.app.model.PlaceMarker;
import androidx.car.app.model.Row;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import java.util.ArrayList;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
index fdc2329..9040f58 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/common/Utils.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/common/Utils.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.common;
+package androidx.car.app.sample.showcase.common;
import android.text.Spannable;
import android.text.SpannableString;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
index 9c2cddd..b4457a7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/FinishAppScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/FinishAppScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
@@ -26,7 +26,7 @@
import androidx.car.app.model.Action;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.ShowcaseService;
+import androidx.car.app.sample.showcase.ShowcaseService;
/** A {@link Screen} that provides an action to exit the car app. */
public class FinishAppScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
index 2e2c350..37afcd1 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/GoToPhoneScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/GoToPhoneScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
index 0aed332..719cf61 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/LoadingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/LoadingDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
index 6f8d97a..b8ecfd8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/MiscDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/MiscDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
index 7a578e0..bd1de79 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/NotificationDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/NotificationDemoScreen.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_CANCEL_RESERVATION;
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_PHONE;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_CANCEL_RESERVATION;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_PHONE;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -48,8 +48,8 @@
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Template;
import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.showcase.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.showcase.R;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.graphics.drawable.IconCompat;
@@ -70,9 +70,9 @@
private static final CharSequence NOTIFICATION_CHANNEL_LOW_NAME = "Low Channel";
private static final String INTENT_ACTION_PRIMARY_PHONE =
- "androidx.car.app.samples.showcase.INTENT_ACTION_PRIMARY_PHONE";
+ "androidx.car.app.sample.showcase.INTENT_ACTION_PRIMARY_PHONE";
private static final String INTENT_ACTION_SECONDARY_PHONE =
- "androidx.car.app.samples.showcase.INTENT_ACTION_SECONDARY_PHONE";
+ "androidx.car.app.sample.showcase.INTENT_ACTION_SECONDARY_PHONE";
private static final int MSG_SEND_NOTIFICATION = 1;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
similarity index 94%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
index 493eec5..c452596 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/OnPhoneActivity.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/OnPhoneActivity.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import android.app.Activity;
import android.content.Intent;
@@ -23,7 +23,7 @@
import android.view.WindowManager.LayoutParams;
import androidx.annotation.Nullable;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
/** Displays a simple {@link Activity} on the phone to show phone/car interaction */
public class OnPhoneActivity extends Activity {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
index f50ad0d..12b5c64 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PopToDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PopToDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
index 52ddf5a..1acfb06 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/PreSeedingFlowScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/PreSeedingFlowScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
index 58bdd9b..ad3d7fe 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/misc/ReservationCancelledScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/misc/ReservationCancelledScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.misc;
+package androidx.car.app.sample.showcase.misc;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
index 124e1d6..8ab0795 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationDemosScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
@@ -25,8 +25,8 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
-import androidx.car.app.samples.showcase.navigation.routing.NavigationTemplateDemoScreen;
+import androidx.car.app.sample.showcase.R;
+import androidx.car.app.sample.showcase.navigation.routing.NavigationTemplateDemoScreen;
import androidx.core.graphics.drawable.IconCompat;
/** A screen showing a list of navigation demos */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
index 1057cee..479463f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationMapOnlyScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationMapOnlyScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
index 98c44a2..e8bcea7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationService.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationService.java
@@ -14,9 +14,9 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
-import static androidx.car.app.samples.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
+import static androidx.car.app.sample.showcase.DeepLinkNotificationReceiver.INTENT_ACTION_NAV_NOTIFICATION_OPEN_APP;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -38,8 +38,8 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.car.app.notification.CarAppExtender;
-import androidx.car.app.samples.showcase.DeepLinkNotificationReceiver;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.DeepLinkNotificationReceiver;
+import androidx.car.app.sample.showcase.R;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
index f601e68..70a4d81 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/NavigationNotificationsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/NavigationNotificationsDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import android.annotation.SuppressLint;
import android.content.Context;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
similarity index 94%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
index 5d090be..96f97e6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/PlaceListNavigationTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
@@ -23,7 +23,7 @@
import androidx.car.app.model.ActionStrip;
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.PlaceListNavigationTemplate;
-import androidx.car.app.samples.showcase.common.SamplePlaces;
+import androidx.car.app.sample.showcase.common.SamplePlaces;
/** Creates a screen using the {@link PlaceListNavigationTemplate} */
public final class PlaceListNavigationTemplateDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
similarity index 76%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
index 77469bd..2393859 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/RoutePreviewDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/RoutePreviewDemoScreen.java
@@ -14,17 +14,20 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import static androidx.car.app.CarToast.LENGTH_LONG;
import android.text.SpannableString;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.Screen;
+import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.model.Action;
+import androidx.car.app.model.CarText;
import androidx.car.app.model.DurationSpan;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Row;
@@ -39,16 +42,30 @@
super(carContext);
}
+ @OptIn(markerClass = ExperimentalCarApi.class)
@NonNull
@Override
public Template onGetTemplate() {
- SpannableString firstRoute = new SpannableString(" \u00b7 Shortest route");
- firstRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+ // Set text variants for the first route.
+ SpannableString firstRouteLongText = new SpannableString(
+ " \u00b7 ---------------- Short" + " " + "route " + "-------------------");
+ firstRouteLongText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+ SpannableString firstRouteShortText = new SpannableString(" \u00b7 Short Route");
+ firstRouteShortText.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(26)), 0, 1, 0);
+ CarText firstRoute = new CarText.Builder(firstRouteLongText)
+ .addVariant(firstRouteShortText)
+ .build();
+
SpannableString secondRoute = new SpannableString(" \u00b7 Less busy");
secondRoute.setSpan(DurationSpan.create(TimeUnit.HOURS.toSeconds(24)), 0, 1, 0);
SpannableString thirdRoute = new SpannableString(" \u00b7 HOV friendly");
thirdRoute.setSpan(DurationSpan.create(TimeUnit.MINUTES.toSeconds(867)), 0, 1, 0);
+ // Set text variants for the navigate action text.
+ CarText navigateActionText =
+ new CarText.Builder("Continue to start navigation").addVariant("Continue to "
+ + "route").build();
+
return new RoutePreviewNavigationTemplate.Builder()
.setItemList(
new ItemList.Builder()
@@ -72,7 +89,7 @@
.build())
.setNavigateAction(
new Action.Builder()
- .setTitle("Continue to route")
+ .setTitle(navigateActionText)
.setOnClickListener(this::onNavigate)
.build())
.setTitle("Routes")
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
similarity index 99%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
index 370fd1e..1df7db8 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/SurfaceRenderer.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/SurfaceRenderer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation;
+package androidx.car.app.sample.showcase.navigation;
import android.graphics.Canvas;
import android.graphics.Color;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
index 386cf83..8a0be87 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/ArrivedDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/ArrivedDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -28,7 +28,7 @@
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.MessageInfo;
import androidx.car.app.navigation.model.NavigationTemplate;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
similarity index 95%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
index 3281f8a..769de10 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/JunctionImageDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/JunctionImageDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
@@ -24,7 +24,7 @@
import androidx.car.app.model.Template;
import androidx.car.app.navigation.model.NavigationTemplate;
import androidx.car.app.navigation.model.RoutingInfo;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
index ff6b8f7..53549f6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/LoadingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/LoadingDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
index 0d1ab66..a2845e3a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigatingDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigatingDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
index 30b5ca9..a3c4b9d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/NavigationTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/NavigationTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
index 0941b49..9920f2d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/navigation/routing/RoutingDemoModels.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/navigation/routing/RoutingDemoModels.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.navigation.routing;
+package androidx.car.app.sample.showcase.navigation.routing;
import static androidx.car.app.navigation.model.LaneDirection.SHAPE_NORMAL_RIGHT;
import static androidx.car.app.navigation.model.LaneDirection.SHAPE_STRAIGHT;
@@ -38,7 +38,7 @@
import androidx.car.app.navigation.model.Maneuver;
import androidx.car.app.navigation.model.Step;
import androidx.car.app.navigation.model.TravelEstimate;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import java.util.TimeZone;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
index 239efec..32fbab7 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/GridTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/GridTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.CarToast.LENGTH_LONG;
import static androidx.car.app.model.Action.BACK;
@@ -36,7 +36,7 @@
import androidx.car.app.model.GridTemplate;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
similarity index 79%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
index b67d75d..b1f502c 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/ListTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/ListTemplateDemoScreen.java
@@ -14,17 +14,20 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.CarToast.LENGTH_LONG;
import static androidx.car.app.model.Action.BACK;
import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
import androidx.car.app.CarContext;
import androidx.car.app.CarToast;
import androidx.car.app.Screen;
+import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.model.Action;
import androidx.car.app.model.ActionStrip;
+import androidx.car.app.model.CarText;
import androidx.car.app.model.ItemList;
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.ParkedOnlyOnClickListener;
@@ -44,6 +47,7 @@
@NonNull
@Override
+ @OptIn(markerClass = ExperimentalCarApi.class)
public Template onGetTemplate() {
ItemList.Builder listBuilder = new ItemList.Builder();
@@ -56,13 +60,22 @@
.build());
for (int i = 2; i <= 6; ++i) {
+ // For row text, set text variants that fit best in different screen sizes.
+ String secondTextStr = "Second line of text";
+ CarText secondText =
+ new CarText.Builder(
+ "================= " + secondTextStr + " ================")
+ .addVariant("--------------------- " + secondTextStr
+ + " ----------------------")
+ .addVariant(secondTextStr)
+ .build();
final String onClickText = "Clicked row: " + i;
listBuilder.addItem(
new Row.Builder()
.setOnClickListener(() -> onClick(onClickText))
.setTitle("Title " + i)
.addText("First line of text")
- .addText("Second line of text")
+ .addText(secondText)
.build());
}
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
index b25e010..0dc4ef6 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MessageTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MessageTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.model.Action.BACK;
@@ -26,7 +26,7 @@
import androidx.car.app.model.CarIcon;
import androidx.car.app.model.MessageTemplate;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
/** A screen that demonstrates the message template. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
similarity index 98%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
index bf49fa3..77b2440 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/MiscTemplateDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/MiscTemplateDemosScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
index f525791..c3d2f5a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PaneTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PaneTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.CarToast.LENGTH_SHORT;
@@ -35,7 +35,7 @@
import androidx.car.app.model.PaneTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
similarity index 92%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
index 1ebd95f..ff41412 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/PlaceListTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/PlaceListTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import androidx.annotation.NonNull;
import androidx.car.app.CarContext;
@@ -22,7 +22,7 @@
import androidx.car.app.model.Action;
import androidx.car.app.model.PlaceListMapTemplate;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.common.SamplePlaces;
+import androidx.car.app.sample.showcase.common.SamplePlaces;
/** Creates a screen using the {@link PlaceListMapTemplate} */
public final class PlaceListTemplateDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
index 85d3466..15a954b 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/templates/SearchTemplateDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/templates/SearchTemplateDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.templates;
+package androidx.car.app.sample.showcase.templates;
import static androidx.car.app.CarToast.LENGTH_LONG;
import static androidx.car.app.CarToast.LENGTH_SHORT;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
index da9f030..807ba03 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ColoredTextDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ColoredTextDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import static androidx.car.app.model.Action.BACK;
import static androidx.car.app.model.CarColor.BLUE;
@@ -31,7 +31,7 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.common.Utils;
+import androidx.car.app.sample.showcase.common.Utils;
/** Creates a screen that demonstrate the usage of colored text in the library. */
public final class ColoredTextDemoScreen extends Screen {
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
index 21214d7..4a9c684 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/ContentProviderIconsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/ContentProviderIconsDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import static androidx.car.app.model.Action.BACK;
@@ -30,7 +30,7 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
import java.io.IOException;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
index b528b28..7bb076a 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/DelayedFileProvider.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/DelayedFileProvider.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import android.content.Context;
import android.graphics.Bitmap;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
index 296ff47..5ad799b 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/IconsDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/IconsDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import static androidx.car.app.model.Action.BACK;
import static androidx.car.app.model.CarColor.GREEN;
@@ -29,7 +29,7 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
+import androidx.car.app.sample.showcase.R;
import androidx.core.graphics.drawable.IconCompat;
/** Creates a screen that demonstrate the usage of icons in the library. */
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
similarity index 96%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
index 0d565d9..096ac9d 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/RowDemoScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/RowDemoScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import static androidx.car.app.model.Action.BACK;
import static androidx.car.app.model.CarColor.YELLOW;
@@ -29,8 +29,8 @@
import androidx.car.app.model.ListTemplate;
import androidx.car.app.model.Row;
import androidx.car.app.model.Template;
-import androidx.car.app.samples.showcase.R;
-import androidx.car.app.samples.showcase.common.Utils;
+import androidx.car.app.sample.showcase.R;
+import androidx.car.app.sample.showcase.common.Utils;
import androidx.core.graphics.drawable.IconCompat;
import androidx.lifecycle.DefaultLifecycleObserver;
diff --git a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
similarity index 97%
rename from car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java
rename to car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
index 34e5b5a..747350f 100644
--- a/car/app/app-samples/showcase/src/main/java/androidx/car/app/samples/showcase/textandicons/TextAndIconsDemosScreen.java
+++ b/car/app/app-samples/showcase/src/main/java/androidx/car/app/sample/showcase/textandicons/TextAndIconsDemosScreen.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.samples.showcase.textandicons;
+package androidx.car.app.sample.showcase.textandicons;
import static androidx.car.app.model.Action.BACK;
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 0201474..c340f85 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -481,6 +481,14 @@
method public void onClick();
}
+ @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedDelegate {
+ method public void sendInputCompleted(String, androidx.car.app.OnDoneCallback);
+ }
+
+ @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedListener {
+ method public void onInputCompleted(String);
+ }
+
public interface OnItemVisibilityChangedDelegate {
method public void sendItemVisibilityChanged(int, int, androidx.car.app.OnDoneCallback);
}
@@ -694,7 +702,7 @@
method public androidx.car.app.model.CarText? getHint();
method public int getInputType();
method public int getKeyboardType();
- method public androidx.car.app.model.signin.OnInputCompletedDelegate getOnInputCompletedDelegate();
+ method public androidx.car.app.model.OnInputCompletedDelegate getOnInputCompletedDelegate();
method public boolean isShowKeyboardByDefault();
field public static final int INPUT_TYPE_DEFAULT = 1; // 0x1
field public static final int INPUT_TYPE_PASSWORD = 2; // 0x2
@@ -705,7 +713,7 @@
}
public static final class InputSignInMethod.Builder {
- ctor public InputSignInMethod.Builder(androidx.car.app.model.signin.InputSignInMethod.OnInputCompletedListener);
+ ctor public InputSignInMethod.Builder(androidx.car.app.model.OnInputCompletedListener);
method public androidx.car.app.model.signin.InputSignInMethod build();
method public androidx.car.app.model.signin.InputSignInMethod.Builder setDefaultValue(String);
method public androidx.car.app.model.signin.InputSignInMethod.Builder setErrorMessage(CharSequence);
@@ -715,14 +723,6 @@
method public androidx.car.app.model.signin.InputSignInMethod.Builder setShowKeyboardByDefault(boolean);
}
- public static interface InputSignInMethod.OnInputCompletedListener {
- method public void onInputCompleted(String);
- }
-
- @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public interface OnInputCompletedDelegate {
- method public void sendInputCompleted(String, androidx.car.app.OnDoneCallback);
- }
-
@androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(2) public final class PinSignInMethod implements androidx.car.app.model.signin.SignInTemplate.SignInMethod {
method public String getPin();
}
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
similarity index 95%
rename from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
rename to car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
index e022dc3..632f921 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/car/app/app/src/main/aidl/androidx/car/app/model/IOnInputCompletedListener.aidl
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
import androidx.car.app.IOnDoneCallback;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
similarity index 97%
rename from car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
index 73bf42a..4fcb34d 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegate.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
import android.annotation.SuppressLint;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
similarity index 93%
rename from car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java
rename to car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
index d9d5141..50b85ad 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/OnInputCompletedDelegateImpl.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedDelegateImpl.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.car.app.model;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
@@ -30,7 +30,6 @@
import androidx.car.app.IOnDoneCallback;
import androidx.car.app.OnDoneCallback;
import androidx.car.app.annotations.ExperimentalCarApi;
-import androidx.car.app.model.signin.InputSignInMethod.OnInputCompletedListener;
import androidx.car.app.utils.RemoteUtils;
/**
@@ -57,10 +56,11 @@
}
}
+ /** Creates an instance of {@link OnInputCompletedDelegateImpl}. */
// This mirrors the AIDL class and is not supposed to support an executor as an input.
@SuppressLint("ExecutorRegistration")
@NonNull
- static OnInputCompletedDelegate create(@NonNull OnInputCompletedListener listener) {
+ public static OnInputCompletedDelegate create(@NonNull OnInputCompletedListener listener) {
return new OnInputCompletedDelegateImpl(requireNonNull(listener));
}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java
new file mode 100644
index 0000000..7435ac9
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/model/OnInputCompletedListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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.model;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+
+/** A listener for handling text input completion event. */
+@ExperimentalCarApi
+@RequiresCarApi(2)
+public interface OnInputCompletedListener {
+ /**
+ * Notifies when the user finished entering text in an input box.
+ *
+ * <p>This event is sent when the user finishes typing in the keyboard and pressed enter.
+ * If the user simply stops typing and closes the keyboard, this event will not be sent.
+ *
+ * @param text the text that was entered, or an empty string if no text was typed.
+ */
+ void onInputCompleted(@NonNull String text);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java b/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
index 3b63945..aea8f80 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/signin/InputSignInMethod.java
@@ -31,6 +31,9 @@
import androidx.car.app.annotations.ExperimentalCarApi;
import androidx.car.app.annotations.RequiresCarApi;
import androidx.car.app.model.CarText;
+import androidx.car.app.model.OnInputCompletedDelegate;
+import androidx.car.app.model.OnInputCompletedDelegateImpl;
+import androidx.car.app.model.OnInputCompletedListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -45,19 +48,6 @@
@ExperimentalCarApi
@RequiresCarApi(2)
public final class InputSignInMethod implements SignInTemplate.SignInMethod {
- /** A listener for handling text input completion event. */
- public interface OnInputCompletedListener {
- /**
- * Notifies when the user finished entering text in an input box.
- *
- * <p>This event is sent when the user finishes typing in the keyboard and pressed enter.
- * If the user simply stops typing and closes the keyboard, this event will not be sent.
- *
- * @param text the text that was entered, or an empty string if no text was typed.
- */
- void onInputCompleted(@NonNull String text);
- }
-
/**
* The type of input represented by the {@link InputSignInMethod} instance.
*
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
index 47ff35a..a7a2d10 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/NavigationTemplate.java
@@ -208,7 +208,8 @@
* Sets the {@link TravelEstimate} to the final destination.
*
* @throws IllegalArgumentException if the {@link TravelEstimate}'s remaining time is
- * less than zero
+ * {@link TravelEstimate#REMAINING_TIME_UNKNOWN} or less
+ * than zero
* @throws NullPointerException if {@code destinationTravelEstimate} is {@code null}
*/
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
index 42b162c..558bc8d 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/TravelEstimate.java
@@ -213,6 +213,10 @@
*
* <p>If not set, {@link #REMAINING_TIME_UNKNOWN} will be used.
*
+ * <p>Note that {@link #REMAINING_TIME_UNKNOWN} may not be supported depending on where the
+ * {@link TravelEstimate} is used. See the documentation of where {@link TravelEstimate}
+ * is used for any restrictions that might apply.
+ *
* @throws IllegalArgumentException if {@code remainingTimeSeconds} is a negative value
* but not {@link #REMAINING_TIME_UNKNOWN}
*/
diff --git a/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java b/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
index a2f67f5..eeeaa4e 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/signin/InputSignInMethodTest.java
@@ -27,6 +27,8 @@
import static org.mockito.Mockito.verify;
import androidx.car.app.OnDoneCallback;
+import androidx.car.app.model.OnInputCompletedDelegate;
+import androidx.car.app.model.OnInputCompletedListener;
import org.junit.Rule;
import org.junit.Test;
@@ -45,7 +47,7 @@
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
- InputSignInMethod.OnInputCompletedListener mListener;
+ OnInputCompletedListener mListener;
@Test
public void create_defaultValues() {
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
index 78a236a..bc0c712 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
@@ -23,7 +23,6 @@
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -185,7 +184,6 @@
}
}
- @OptIn(ExperimentalCoroutinesApi::class)
@Test
fun animateToWithInterruption() {
runBlocking {
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
index 72c6567..55d263b 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/SuspendAnimationTest.kt
@@ -21,7 +21,6 @@
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@@ -29,7 +28,6 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(JUnit4::class)
class SuspendAnimationTest {
@Test
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
index d03313b..8ba6ba8 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringChainDemo.kt
@@ -16,15 +16,23 @@
package androidx.compose.animation.demos
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Icon
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
@@ -35,8 +43,10 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
@@ -55,34 +65,56 @@
modifier = Modifier.align(Alignment.Center),
text = "Since we are here, why not drag me around?"
)
- val size = pastelAwakening.size
+ val size = vibrantColors.size
val followers = remember { Array<State<Offset>>(size) { mutableStateOf(Offset.Zero) } }
for (i in 0 until size) {
// Each follower on the spring chain uses the previous follower's position as target
followers[i] = animateOffsetAsState(if (i == 0) leader else followers[i - 1].value)
}
+ var expanded by remember { mutableStateOf(false) }
+ // Put space between followers when expanded
+ val spacing by animateIntAsState(if (expanded) -300 else 0, spring(dampingRatio = 0.7f))
+
// Followers stacked in reverse orders
for (i in followers.size - 1 downTo 0) {
Box(
Modifier
.offset { followers[i].value.round() }
- .size(80.dp)
- .background(pastelAwakening[i], CircleShape)
+ .offset { IntOffset(0, spacing * (i + 1)) }
+ .size(circleSize)
+ .background(vibrantColors[i], CircleShape)
)
}
+
// Leader
Box(
- Modifier.offset { leader.round() }.size(80.dp)
- .background(Color(0xFFfffbd0), CircleShape)
- )
+ Modifier.offset { leader.round() }.size(circleSize)
+ .clickable(
+ indication = null,
+ interactionSource = remember { MutableInteractionSource() }
+ ) { expanded = !expanded }
+ .background(Color(0xFFfff8ad), CircleShape)
+ ) {
+ // Rotate icon when expanded / collapsed
+ val rotation by animateFloatAsState(if (expanded) 180f else 0f)
+ Icon(
+ Icons.Filled.KeyboardArrowDown,
+ contentDescription = "Expand or Collapse",
+ modifier = Modifier.size(30.dp).align(Alignment.Center)
+ .graphicsLayer { this.rotationZ = rotation },
+ tint = Color.Gray
+ )
+ }
}
}
-private val pastelAwakening = listOf(
- Color(0xffdfdeff),
- Color(0xffffe0f5),
- Color(0xffffefd8),
- Color(0xffe6ffd0),
- Color(0xffd9f6ff)
+val circleSize = 60.dp
+
+private val vibrantColors = listOf(
+ Color(0xffbfbdff),
+ Color(0xffffc7ed),
+ Color(0xffffdcab),
+ Color(0xffd5ffb0),
+ Color(0xffbaefff)
)
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index 2657faf..91c88cc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -18,8 +18,6 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static androidx.build.dependencies.DependenciesKt.*
-import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
plugins {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
index 49f83020..c091eea 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenSignatureTest.kt
@@ -273,6 +273,7 @@
$text
+ fun used(x: Any?) {}
""",
dumpClasses
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
index 9fb039e..64dc648 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCodegenTest.kt
@@ -87,6 +87,8 @@
import androidx.compose.runtime.*
$src
+
+ fun used(x: Any?) {}
""",
fileName, dumpClasses
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
index 6355b8b..c9657c0 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
@@ -23,7 +23,8 @@
@Language("kotlin")
source: String,
expectedTransformed: String,
- dumpTree: Boolean = false
+ dumpTree: Boolean = false,
+ compilation: Compilation = JvmCompilation()
) = verifyComposeIrTransform(
"""
import androidx.compose.runtime.Composable
@@ -37,6 +38,7 @@
import androidx.compose.runtime.Composable
inline class InlineClass(val value: Int)
+ fun used(x: Any?) {}
@Composable fun A() {}
@Composable fun A(x: Int) { }
@@ -56,6 +58,7 @@
var b = 2
var c = 3
""".trimIndent(),
- dumpTree = dumpTree
+ dumpTree = dumpTree,
+ compilation = compilation
)
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 81efc17..929bc3c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -69,6 +69,7 @@
abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
open val liveLiteralsEnabled get() = false
open val sourceInformationEnabled get() = true
+
private val extension = ComposeIrGenerationExtension(
liveLiteralsEnabled,
sourceInformationEnabled,
@@ -77,6 +78,7 @@
// Some tests require the plugin context in order to perform assertions, for example, a
// context is required to determine the stability of a type using the StabilityInferencer.
var pluginContext: IrPluginContext? = null
+
override fun postProcessingStep(
module: IrModuleFragment,
generatorContext: GeneratorContext,
@@ -163,13 +165,19 @@
expectedTransformed: String,
extra: String = "",
validator: (element: IrElement) -> Unit = { },
- dumpTree: Boolean = false
+ dumpTree: Boolean = false,
+ compilation: Compilation = JvmCompilation()
) {
+ if (!compilation.enabled) {
+ // todo indicate ignore?
+ return
+ }
+
val files = listOf(
sourceFile("Test.kt", source.replace('%', '$')),
sourceFile("Extra.kt", extra.replace('%', '$'))
)
- val irModule = generateIrModuleWithJvmResolve(files)
+ val irModule = compilation.compile(files)
val keySet = mutableListOf<Int>()
fun IrElement.validate(): IrElement = this.also { validator(it) }
val actualTransformed = irModule
@@ -344,116 +352,6 @@
return result
}
- protected fun generateIrModuleWithJvmResolve(files: List<KtFile>): IrModuleFragment {
- val classPath = createClasspath() + additionalPaths
- val configuration = newConfiguration()
- configuration.addJvmClasspathRoots(classPath)
- configuration.put(JVMConfigurationKeys.IR, true)
- configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-
- val environment = KotlinCoreEnvironment.createForTests(
- myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
- ).also { setupEnvironment(it) }
-
- val mangler = JvmManglerDesc(null)
-
- val psi2ir = Psi2IrTranslator(
- environment.configuration.languageVersionSettings,
- Psi2IrConfiguration(ignoreErrors = false)
- )
- val symbolTable = SymbolTable(
- JvmIdSignatureDescriptor(mangler),
- IrFactoryImpl,
- JvmNameProvider
- )
-
- val analysisResult = JvmResolveUtil.analyze(files, environment)
- if (!psi2ir.configuration.ignoreErrors) {
- analysisResult.throwIfError()
- AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
- }
- val extensions = JvmGeneratorExtensions()
- val generatorContext = psi2ir.createGeneratorContext(
- analysisResult.moduleDescriptor,
- analysisResult.bindingContext,
- symbolTable,
- extensions = extensions
- )
- val stubGenerator = DeclarationStubGenerator(
- generatorContext.moduleDescriptor,
- generatorContext.symbolTable,
- generatorContext.irBuiltIns.languageVersionSettings,
- extensions
- )
- val functionFactory = IrFunctionFactory(
- generatorContext.irBuiltIns,
- generatorContext.symbolTable
- )
- val frontEndContext = object : TranslationPluginContext {
- override val moduleDescriptor: ModuleDescriptor
- get() = generatorContext.moduleDescriptor
- override val bindingContext: BindingContext
- get() = generatorContext.bindingContext
- override val symbolTable: ReferenceSymbolTable
- get() = symbolTable
- override val typeTranslator: TypeTranslator
- get() = generatorContext.typeTranslator
- override val irBuiltIns: IrBuiltIns
- get() = generatorContext.irBuiltIns
- }
- generatorContext.irBuiltIns.functionFactory = functionFactory
- val irLinker = JvmIrLinker(
- generatorContext.moduleDescriptor,
- EmptyLoggingContext,
- generatorContext.irBuiltIns,
- generatorContext.symbolTable,
- functionFactory,
- frontEndContext,
- stubGenerator,
- mangler
- )
-
- generatorContext.moduleDescriptor.allDependencyModules.map {
- val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
- val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
- irLinker.deserializeIrModuleHeader(it, kotlinLibrary)
- }
-
- val irProviders = listOf(irLinker)
-
- val symbols = BuiltinSymbolsBase(
- generatorContext.irBuiltIns,
- generatorContext.moduleDescriptor.builtIns,
- generatorContext.symbolTable.lazyWrapper
- )
-
- ExternalDependenciesGenerator(
- generatorContext.symbolTable,
- irProviders,
- generatorContext.languageVersionSettings
- ).generateUnboundSymbolsAsDependencies()
-
- psi2ir.addPostprocessingStep { module ->
- val old = stubGenerator.unboundSymbolGeneration
- try {
- stubGenerator.unboundSymbolGeneration = true
- postProcessingStep(module, generatorContext, irLinker, symbols)
- } finally {
- stubGenerator.unboundSymbolGeneration = old
- }
- }
-
- val irModuleFragment = psi2ir.generateModuleFragment(
- generatorContext,
- files,
- irProviders,
- IrGenerationExtension.getInstances(myEnvironment!!.project),
- expectDescriptorToSymbol = null
- )
- irLinker.postProcess()
- return irModuleFragment
- }
-
fun facadeClassGenerator(
generatorContext: GeneratorContext,
source: DeserializedContainerSource
@@ -467,4 +365,124 @@
it.createParameterDeclarations()
}
}
+
+ inner class JvmCompilation : Compilation {
+ override val enabled: Boolean = true
+
+ override fun compile(files: List<KtFile>): IrModuleFragment {
+ val classPath = createClasspath() + additionalPaths
+ val configuration = newConfiguration()
+ configuration.addJvmClasspathRoots(classPath)
+ configuration.put(JVMConfigurationKeys.IR, true)
+ configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
+
+ val environment = KotlinCoreEnvironment.createForTests(
+ myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
+ ).also { setupEnvironment(it) }
+
+ val mangler = JvmManglerDesc(null)
+
+ val psi2ir = Psi2IrTranslator(
+ environment.configuration.languageVersionSettings,
+ Psi2IrConfiguration(ignoreErrors = false)
+ )
+ val symbolTable = SymbolTable(
+ JvmIdSignatureDescriptor(mangler),
+ IrFactoryImpl,
+ JvmNameProvider
+ )
+
+ val analysisResult = JvmResolveUtil.analyze(files, environment)
+ if (!psi2ir.configuration.ignoreErrors) {
+ analysisResult.throwIfError()
+ AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
+ }
+ val extensions = JvmGeneratorExtensions()
+ val generatorContext = psi2ir.createGeneratorContext(
+ analysisResult.moduleDescriptor,
+ analysisResult.bindingContext,
+ symbolTable,
+ extensions = extensions
+ )
+ val stubGenerator = DeclarationStubGenerator(
+ generatorContext.moduleDescriptor,
+ generatorContext.symbolTable,
+ generatorContext.irBuiltIns.languageVersionSettings,
+ extensions
+ )
+ val functionFactory = IrFunctionFactory(
+ generatorContext.irBuiltIns,
+ generatorContext.symbolTable
+ )
+ val frontEndContext = object : TranslationPluginContext {
+ override val moduleDescriptor: ModuleDescriptor
+ get() = generatorContext.moduleDescriptor
+ override val bindingContext: BindingContext
+ get() = generatorContext.bindingContext
+ override val symbolTable: ReferenceSymbolTable
+ get() = symbolTable
+ override val typeTranslator: TypeTranslator
+ get() = generatorContext.typeTranslator
+ override val irBuiltIns: IrBuiltIns
+ get() = generatorContext.irBuiltIns
+ }
+ generatorContext.irBuiltIns.functionFactory = functionFactory
+ val irLinker = JvmIrLinker(
+ generatorContext.moduleDescriptor,
+ EmptyLoggingContext,
+ generatorContext.irBuiltIns,
+ generatorContext.symbolTable,
+ functionFactory,
+ frontEndContext,
+ stubGenerator,
+ mangler
+ )
+
+ generatorContext.moduleDescriptor.allDependencyModules.map {
+ val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
+ val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
+ irLinker.deserializeIrModuleHeader(it, kotlinLibrary)
+ }
+
+ val irProviders = listOf(irLinker)
+
+ val symbols = BuiltinSymbolsBase(
+ generatorContext.irBuiltIns,
+ generatorContext.moduleDescriptor.builtIns,
+ generatorContext.symbolTable.lazyWrapper
+ )
+
+ ExternalDependenciesGenerator(
+ generatorContext.symbolTable,
+ irProviders,
+ generatorContext.languageVersionSettings
+ ).generateUnboundSymbolsAsDependencies()
+
+ psi2ir.addPostprocessingStep { module ->
+ val old = stubGenerator.unboundSymbolGeneration
+ try {
+ stubGenerator.unboundSymbolGeneration = true
+ postProcessingStep(module, generatorContext, irLinker, symbols)
+ } finally {
+ stubGenerator.unboundSymbolGeneration = old
+ }
+ }
+
+ val irModuleFragment = psi2ir.generateModuleFragment(
+ generatorContext,
+ files,
+ irProviders,
+ IrGenerationExtension.getInstances(myEnvironment!!.project),
+ expectDescriptorToSymbol = null
+ )
+ irLinker.postProcess()
+ return irModuleFragment
+ }
+ }
+
+ // This interface enables different Compilation variants for compiler tests
+ interface Compilation {
+ val enabled: Boolean
+ fun compile(files: List<KtFile>): IrModuleFragment
+ }
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index 7562046..84aba6a 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -512,12 +512,14 @@
class UnstableDelegateProp {
var p1 by UnstableDelegate()
}
+ fun used(x: Any?) {}
""",
"""
import a.*
import androidx.compose.runtime.Composable
@Composable fun A(y: Any) {
+ used(y)
A(EmptyClass())
A(SingleStableValInt(123))
A(SingleStableVal(StableClass()))
@@ -541,6 +543,7 @@
@Composable
fun A(y: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A)<A(Empt...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Sing...>,<A(Doub...>,<A(Doub...>,<A(Doub...>,<A(Doub...>,<A(X(li...>,<A(X(li...>,<A(NonB...>,<A(NonB...>,<A(Stab...>,<A(Unst...>:Test.kt")
+ used(y)
A(EmptyClass(), %composer, EmptyClass.%stable)
A(SingleStableValInt(123), %composer, SingleStableValInt.%stable)
A(SingleStableVal(StableClass()), %composer, SingleStableVal.%stable)
@@ -609,6 +612,7 @@
get() { TODO() }
set(value) { }
}
+ fun used(x: Any?) {}
""",
"""
import a.*
@@ -622,6 +626,7 @@
var p1 by UnstableDelegate()
}
@Composable fun A(y: Any) {
+ used(y)
A(X(listOf(StableClass())))
A(StableDelegateProp())
A(UnstableDelegateProp())
@@ -657,6 +662,7 @@
@Composable
fun A(y: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A)<A(X(li...>,<A(Stab...>,<A(Unst...>:Test.kt")
+ used(y)
A(X(listOf(StableClass())), %composer, 0b1000)
A(StableDelegateProp(), %composer, 0)
A(UnstableDelegateProp(), %composer, UnstableDelegate.%stable)
@@ -675,13 +681,14 @@
fun make(): T = error("")
}
class Foo
+ fun used(x: Any?) {}
""",
"""
- import a.Wrapper
- import a.Foo
+ import a.*
import androidx.compose.runtime.Composable
@Composable fun A(y: Any) {
+ used(y)
A(Wrapper(Foo()))
}
""",
@@ -689,6 +696,7 @@
@Composable
fun A(y: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A)<A(Wrap...>:Test.kt")
+ used(y)
A(Wrapper(Foo()), %composer, Wrapper.%stable)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
A(y, %composer, %changed or 0b0001)
@@ -742,10 +750,11 @@
@Composable
fun <T> X(items: List<T>, itemContent: Function3<T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(X)P(1)*<itemCo...>:Test.kt")
+ val %dirty = %changed
val tmp0_iterator = items.iterator()
while (tmp0_iterator.hasNext()) {
val item = tmp0_iterator.next()
- itemContent(item, %composer, 0b01110000 and %changed)
+ itemContent(item, %composer, 0b01110000 and %dirty)
}
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
X(items, itemContent, %composer, %changed or 0b0001)
@@ -801,9 +810,12 @@
class Foo
@Composable fun A(y: Int, x: Any) {
+ used(y)
B(x)
}
- @Composable fun B(x: Any) {}
+ @Composable fun B(x: Any) {
+ used(x)
+ }
""",
"""
@StabilityInferred(parameters = 0)
@@ -813,6 +825,7 @@
@Composable
fun A(y: Int, x: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A)P(1)<B(x)>:Test.kt")
+ used(y)
B(x, %composer, 0b1000)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
A(y, x, %composer, %changed or 0b0001)
@@ -821,6 +834,7 @@
@Composable
fun B(x: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
+ used(x)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
B(x, %composer, %changed or 0b0001)
}
@@ -835,9 +849,12 @@
class Foo(var bar: Int = 0)
@Composable fun A(y: Int, x: Foo) {
+ used(y)
B(x)
}
- @Composable fun B(x: Any) {}
+ @Composable fun B(x: Any) {
+ used(x)
+ }
""",
"""
@StabilityInferred(parameters = 0)
@@ -847,6 +864,7 @@
@Composable
fun A(y: Int, x: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A)P(1)<B(x)>:Test.kt")
+ used(y)
B(x, %composer, 0b1000)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
A(y, x, %composer, %changed or 0b0001)
@@ -855,6 +873,7 @@
@Composable
fun B(x: Any, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
+ used(x)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
B(x, %composer, %changed or 0b0001)
}
@@ -884,7 +903,7 @@
val files = listOf(
sourceFile("Test.kt", source.replace('%', '$'))
)
- val irModule = generateIrModuleWithJvmResolve(files)
+ val irModule = JvmCompilation().compile(files)
val irClass = irModule.files.last().declarations.first() as IrClass
val classStability = StabilityInferencer(pluginContext!!).stabilityOf(irClass.defaultType)
@@ -1006,7 +1025,7 @@
val files = listOf(
sourceFile("Test.kt", source.replace('%', '$'))
)
- return generateIrModuleWithJvmResolve(files)
+ return JvmCompilation().compile(files)
}
private fun assertTransform(
@@ -1018,7 +1037,10 @@
) = verifyComposeIrTransform(
checked,
expectedTransformed,
- unchecked,
+ """
+ $unchecked
+ fun used(x: Any?) {}
+ """,
dumpTree = dumpTree
)
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
index d93c8be..e36dde9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamSignatureTests.kt
@@ -97,6 +97,33 @@
}
@Test
+ fun testStrangeReceiverIssue(): Unit = codegen(
+ """
+ import androidx.compose.runtime.ExplicitGroupsComposable
+ import androidx.compose.runtime.NonRestartableComposable
+ class Foo
+
+ @Composable
+ @ExplicitGroupsComposable
+ fun A(foo: Foo) {
+ foo.b()
+ }
+
+ @Composable
+ @ExplicitGroupsComposable
+ inline fun Foo.b(label: String = "") {
+ c(this, label)
+ }
+
+ @Composable
+ @ExplicitGroupsComposable
+ inline fun c(foo: Foo, label: String) {
+ used(label)
+ }
+ """
+ )
+
+ @Test
fun testInterfaceMethodWithComposableParameter(): Unit = validateBytecode(
"""
@Composable
@@ -139,7 +166,16 @@
f: Float,
g: Long,
h: Double
- ) {}
+ ) {
+ used(a)
+ used(b)
+ used(c)
+ used(d)
+ used(e)
+ used(f)
+ used(g)
+ used(h)
+ }
"""
) {
assert(it.contains("INVOKEINTERFACE androidx/compose/runtime/Composer.changed (Z)Z"))
@@ -159,7 +195,9 @@
import androidx.compose.runtime.Stable
@Stable class Bar
- @Composable fun Foo(a: Bar) {}
+ @Composable fun Foo(a: Bar) {
+ used(a)
+ }
"""
) {
assert(!it.contains("INVOKEINTERFACE androidx/compose/runtime/Composer.changed (Z)Z"))
@@ -177,7 +215,9 @@
fun testInlineClassChangedCalls(): Unit = validateBytecode(
"""
inline class Bar(val value: Int)
- @Composable fun Foo(a: Bar) {}
+ @Composable fun Foo(a: Bar) {
+ used(a)
+ }
"""
) {
assert(!it.contains("INVOKESTATIC Bar.box-impl (I)LBar;"))
@@ -191,7 +231,9 @@
fun testNullableInlineClassChangedCalls(): Unit = validateBytecode(
"""
inline class Bar(val value: Int)
- @Composable fun Foo(a: Bar?) {}
+ @Composable fun Foo(a: Bar?) {
+ used(a)
+ }
"""
) {
val testClass = it.split("public final class ").single { it.startsWith("test/TestKt") }
@@ -1081,11 +1123,9 @@
public final static Wat(Landroidx/compose/runtime/Composer;I)V
public final static Foo(ILandroidx/compose/runtime/Composer;I)V
private final static Foo%goo(Landroidx/compose/runtime/Composer;I)V
- public final static synthetic access%Foo%goo(Landroidx/compose/runtime/Composer;I)V
final static INNERCLASS TestKt%Wat%1 null null
public final static INNERCLASS TestKt%Foo%Bar null Bar
final static INNERCLASS TestKt%Foo%1 null null
- final static INNERCLASS TestKt%Foo%goo%1 null null
}
final class TestKt%Wat%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
<init>(I)V
@@ -1099,17 +1139,6 @@
public <init>()V
public final baz(Landroidx/compose/runtime/Composer;I)V
OUTERCLASS TestKt Foo (ILandroidx/compose/runtime/Composer;I)V
- final static INNERCLASS TestKt%Foo%Bar%baz%1 null null
- public final static INNERCLASS TestKt%Foo%Bar null Bar
- }
- final class TestKt%Foo%Bar%baz%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
- <init>(LTestKt%Foo%Bar;I)V
- public final invoke(Landroidx/compose/runtime/Composer;I)V
- public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
- final synthetic LTestKt%Foo%Bar; %tmp0_rcvr
- final synthetic I %%changed
- OUTERCLASS TestKt%Foo%Bar baz (Landroidx/compose/runtime/Composer;I)V
- final static INNERCLASS TestKt%Foo%Bar%baz%1 null null
public final static INNERCLASS TestKt%Foo%Bar null Bar
}
final class TestKt%Foo%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
@@ -1121,14 +1150,6 @@
OUTERCLASS TestKt Foo (ILandroidx/compose/runtime/Composer;I)V
final static INNERCLASS TestKt%Foo%1 null null
}
- final class TestKt%Foo%goo%1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function2 {
- <init>(I)V
- public final invoke(Landroidx/compose/runtime/Composer;I)V
- public synthetic bridge invoke(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
- final synthetic I %%changed
- OUTERCLASS TestKt Foo%goo (Landroidx/compose/runtime/Composer;I)V
- final static INNERCLASS TestKt%Foo%goo%1 null null
- }
"""
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index a1bb13c..dd98c75 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -47,7 +47,10 @@
$source
""".trimIndent(),
expectedTransformed,
- "",
+ """
+ package test
+ fun used(x: Any?) {}
+ """,
validator,
dumpTree
)
@@ -349,7 +352,9 @@
}
@Composable
- fun Leaf(text: String) { }
+ fun Leaf(text: String) {
+ used(text)
+ }
@Composable
fun Test(value: Int) {
@@ -385,6 +390,7 @@
%dirty = %dirty or if (%composer.changed(text)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ used(text)
} else {
%composer.skipToGroupEnd()
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
index 5a3ba82..d7f5794 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTests.kt
@@ -2430,6 +2430,7 @@
"""
@Composable
fun Test(value: InlineClass) {
+ used(value)
A()
}
""",
@@ -2442,6 +2443,7 @@
%dirty = %dirty or if (%composer.changed(value.value)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ used(value)
A(%composer, 0)
} else {
%composer.skipToGroupEnd()
@@ -2456,30 +2458,150 @@
@Test
fun testParameterOrderInformation(): Unit = controlFlow(
"""
- @Composable fun Test01(p0: Int, p1: Int, p2: Int, p3: Int) { }
- @Composable fun Test02(p0: Int, p1: Int, p3: Int, p2: Int) { }
- @Composable fun Test03(p0: Int, p2: Int, p1: Int, p3: Int) { }
- @Composable fun Test04(p0: Int, p2: Int, p3: Int, p1: Int) { }
- @Composable fun Test05(p0: Int, p3: Int, p1: Int, p2: Int) { }
- @Composable fun Test06(p0: Int, p3: Int, p2: Int, p1: Int) { }
- @Composable fun Test07(p1: Int, p0: Int, p2: Int, p3: Int) { }
- @Composable fun Test08(p1: Int, p0: Int, p3: Int, p2: Int) { }
- @Composable fun Test09(p1: Int, p2: Int, p0: Int, p3: Int) { }
- @Composable fun Test00(p1: Int, p2: Int, p3: Int, p0: Int) { }
- @Composable fun Test11(p1: Int, p3: Int, p0: Int, p2: Int) { }
- @Composable fun Test12(p1: Int, p3: Int, p2: Int, p0: Int) { }
- @Composable fun Test13(p2: Int, p0: Int, p1: Int, p3: Int) { }
- @Composable fun Test14(p2: Int, p0: Int, p3: Int, p1: Int) { }
- @Composable fun Test15(p2: Int, p1: Int, p0: Int, p3: Int) { }
- @Composable fun Test16(p2: Int, p1: Int, p3: Int, p0: Int) { }
- @Composable fun Test17(p2: Int, p3: Int, p0: Int, p1: Int) { }
- @Composable fun Test18(p2: Int, p3: Int, p1: Int, p0: Int) { }
- @Composable fun Test19(p3: Int, p0: Int, p1: Int, p2: Int) { }
- @Composable fun Test20(p3: Int, p0: Int, p2: Int, p1: Int) { }
- @Composable fun Test21(p3: Int, p1: Int, p0: Int, p2: Int) { }
- @Composable fun Test22(p3: Int, p1: Int, p2: Int, p0: Int) { }
- @Composable fun Test23(p3: Int, p2: Int, p0: Int, p1: Int) { }
- @Composable fun Test24(p3: Int, p2: Int, p1: Int, p0: Int) { }
+ @Composable fun Test01(p0: Int, p1: Int, p2: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test02(p0: Int, p1: Int, p3: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test03(p0: Int, p2: Int, p1: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test04(p0: Int, p2: Int, p3: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test05(p0: Int, p3: Int, p1: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test06(p0: Int, p3: Int, p2: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test07(p1: Int, p0: Int, p2: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test08(p1: Int, p0: Int, p3: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test09(p1: Int, p2: Int, p0: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test00(p1: Int, p2: Int, p3: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test11(p1: Int, p3: Int, p0: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test12(p1: Int, p3: Int, p2: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test13(p2: Int, p0: Int, p1: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test14(p2: Int, p0: Int, p3: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test15(p2: Int, p1: Int, p0: Int, p3: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test16(p2: Int, p1: Int, p3: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test17(p2: Int, p3: Int, p0: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test18(p2: Int, p3: Int, p1: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test19(p3: Int, p0: Int, p1: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test20(p3: Int, p0: Int, p2: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test21(p3: Int, p1: Int, p0: Int, p2: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test22(p3: Int, p1: Int, p2: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test23(p3: Int, p2: Int, p0: Int, p1: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
+ @Composable fun Test24(p3: Int, p2: Int, p1: Int, p0: Int) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
+ }
""",
"""
@Composable
@@ -2499,6 +2621,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2523,6 +2649,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2547,6 +2677,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2571,6 +2705,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2595,6 +2733,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2619,6 +2761,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2643,6 +2789,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2667,6 +2817,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2691,6 +2845,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2715,6 +2873,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2739,6 +2901,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2763,6 +2929,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2787,6 +2957,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2811,6 +2985,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2835,6 +3013,10 @@
%dirty = %dirty or if (%composer.changed(p3)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2859,6 +3041,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2883,6 +3069,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2907,6 +3097,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2931,6 +3125,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2955,6 +3153,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -2979,6 +3181,10 @@
%dirty = %dirty or if (%composer.changed(p2)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -3003,6 +3209,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -3027,6 +3237,10 @@
%dirty = %dirty or if (%composer.changed(p1)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -3051,6 +3265,10 @@
%dirty = %dirty or if (%composer.changed(p0)) 0b100000000000 else 0b010000000000
}
if (%dirty and 0b0001011011011011 xor 0b010010010010 !== 0 || !%composer.skipping) {
+ used(p0)
+ used(p1)
+ used(p2)
+ used(p3)
} else {
%composer.skipToGroupEnd()
}
@@ -3070,13 +3288,14 @@
@Composable
fun Test(value: LocalInlineClass) {
-
+ used(value)
}
""",
extra = """
package androidx.compose.runtime.tests
inline class LocalInlineClass(val value: Int)
+ fun used(x: Any?) {}
""",
expectedTransformed = """
@Composable
@@ -3087,6 +3306,7 @@
%dirty = %dirty or if (%composer.changed(value.value)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ used(value)
} else {
%composer.skipToGroupEnd()
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 4b90282..1c8d410 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -40,6 +40,8 @@
import androidx.compose.runtime.NonRestartableComposable
$unchecked
+
+ fun used(x: Any?) {}
""".trimIndent(),
dumpTree = dumpTree
)
@@ -165,7 +167,7 @@
"""
@Composable
fun Test(x: Int = makeInt()) {
-
+ used(x)
}
""",
"""
@@ -190,6 +192,7 @@
%dirty = %dirty and 0b1110.inv()
}
}
+ used(x)
} else {
%composer.skipToGroupEnd()
}
@@ -292,7 +295,37 @@
a29: Int = 0,
a30: Int = 0
) {
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
}
""",
"""
@@ -552,7 +585,37 @@
if (%default and 0b01000000000000000000000000000000 !== 0) {
a30 = 0
}
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
} else {
%composer.skipToGroupEnd()
}
@@ -603,7 +666,38 @@
a30: Int = 0,
a31: Int = 0
) {
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
+ used(a31)
}
""",
"""
@@ -871,7 +965,38 @@
if (%default1 and 0b0001 !== 0) {
a31 = 0
}
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
+ used(a31)
} else {
%composer.skipToGroupEnd()
}
@@ -923,7 +1048,38 @@
a30: Int = 0,
a31: Foo = Foo()
) {
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
+ used(a31)
}
""",
"""
@@ -1201,7 +1357,38 @@
%dirty3 = %dirty3 and 0b01110000.inv()
}
}
- print("Hello world!")
+ used(a00)
+ used(a01)
+ used(a02)
+ used(a03)
+ used(a04)
+ used(a05)
+ used(a06)
+ used(a07)
+ used(a08)
+ used(a09)
+ used(a10)
+ used(a11)
+ used(a12)
+ used(a13)
+ used(a14)
+ used(a15)
+ used(a16)
+ used(a17)
+ used(a18)
+ used(a19)
+ used(a20)
+ used(a21)
+ used(a22)
+ used(a23)
+ used(a24)
+ used(a25)
+ used(a26)
+ used(a27)
+ used(a28)
+ used(a29)
+ used(a30)
+ used(a31)
} else {
%composer.skipToGroupEnd()
}
@@ -1246,7 +1433,7 @@
@Composable
fun Example(%composer: Composer?, %changed: Int) {
%composer.startReplaceableGroup(<>, "C(Example)<foo()>:Test.kt")
- foo(0, %composer, 0, 0b0001)
+ foo(0, %composer, 0b01110000 and %changed shl 0b0011, 0b0001)
%composer.endReplaceableGroup()
}
static val %stable: Int = 0
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index b27a838..b07a52e 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -40,6 +40,7 @@
import androidx.compose.runtime.Composable
$unchecked
+ fun used(x: Any?) {}
""".trimIndent(),
dumpTree = dumpTree
)
@@ -55,6 +56,7 @@
"""
@Composable
fun Test(x: Int = 0, y: Int = 0) {
+ used(y)
Wrap {
if (x > 0) {
A(x)
@@ -86,6 +88,7 @@
if (%default and 0b0010 !== 0) {
y = 0
}
+ used(y)
Wrap(composableLambda(%composer, <>, true, "C:Test.kt") { %composer: Composer?, %changed: Int ->
if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
if (x > 0) {
@@ -120,11 +123,13 @@
""",
"""
fun Example(a: A) {
+ used(a)
Example { it -> a.compute(it) }
}
""",
"""
fun Example(a: A) {
+ used(a)
Example(class <no name provided> : A {
@Composable
override fun compute(it: Int, %composer: Composer?, %changed: Int) {
@@ -133,8 +138,7 @@
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
}
- %dirty = %dirty or 0b00110000
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
a.compute(it, %composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
@@ -273,7 +277,12 @@
crossAxisSize: SizeMode = SizeMode.Wrap,
content: @Composable() ()->Unit
) {
- println()
+ used(orientation)
+ used(modifier)
+ used(arrangement)
+ used(crossAxisAlignment)
+ used(crossAxisSize)
+ content()
}
@Composable
@@ -296,7 +305,7 @@
"""
@Composable
fun RowColumnImpl(orientation: LayoutOrientation, modifier: Modifier?, arrangement: Vertical?, crossAxisAlignment: Horizontal?, crossAxisSize: SizeMode?, content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int, %default: Int) {
- %composer = %composer.startRestartGroup(<>, "C(RowColumnImpl)P(5,4!1,2,3):Test.kt")
+ %composer = %composer.startRestartGroup(<>, "C(RowColumnImpl)P(5,4!1,2,3)<conten...>:Test.kt")
val %dirty = %changed
if (%default and 0b0001 !== 0) {
%dirty = %dirty or 0b0110
@@ -349,7 +358,12 @@
%dirty = %dirty and 0b0001110000000000.inv()
}
}
- println()
+ used(orientation)
+ used(modifier)
+ used(arrangement)
+ used(crossAxisAlignment)
+ used(crossAxisSize)
+ content(%composer, 0b1110 and %dirty shr 0b1111)
} else {
%composer.skipToGroupEnd()
}
@@ -425,7 +439,7 @@
"""
@Composable
fun SimpleBox(modifier: Modifier = Modifier) {
- println()
+ used(modifier)
}
""",
"""
@@ -442,7 +456,7 @@
if (%default and 0b0001 !== 0) {
modifier = Companion
}
- println()
+ used(modifier)
} else {
%composer.skipToGroupEnd()
}
@@ -498,6 +512,46 @@
)
@Test
+ fun testLocalComposableFunctions(): Unit = comparisonPropagation(
+ """
+ @Composable fun A(a: Int) {}
+ """,
+ """
+ @Composable
+ fun Example(a: Int) {
+ @Composable fun Inner() {
+ A(a)
+ }
+ Inner()
+ }
+ """,
+ """
+ @Composable
+ fun Example(a: Int, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Example)<Inner(...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ @Composable
+ fun Inner(%composer: Composer?, %changed: Int) {
+ %composer.startReplaceableGroup(<>, "C(Inner)<A(a)>:Test.kt")
+ A(a, %composer, 0b1110 and %dirty)
+ %composer.endReplaceableGroup()
+ }
+ Inner(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Example(a, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
fun testLoopWithContinueAndCallAfter(): Unit = comparisonPropagation(
"""
@Composable fun Call() {}
@@ -557,7 +611,8 @@
"""
@Composable
fun SimpleBox(modifier: Modifier = Modifier, shape: Shape = RectangleShape) {
- println()
+ used(modifier)
+ used(shape)
}
""",
"""
@@ -590,7 +645,8 @@
%dirty = %dirty and 0b01110000.inv()
}
}
- println()
+ used(modifier)
+ used(shape)
} else {
%composer.skipToGroupEnd()
}
@@ -614,13 +670,14 @@
"""
@Composable
fun SimpleBox(modifier: Modifier = Modifier, content: @Composable() () -> Unit = {}) {
- println()
+ used(modifier)
+ content()
}
""",
"""
@Composable
fun SimpleBox(modifier: Modifier?, content: Function2<Composer, Int, Unit>?, %composer: Composer?, %changed: Int, %default: Int) {
- %composer = %composer.startRestartGroup(<>, "C(SimpleBox)P(1):Test.kt")
+ %composer = %composer.startRestartGroup(<>, "C(SimpleBox)P(1)<conten...>:Test.kt")
val %dirty = %changed
if (%default and 0b0001 !== 0) {
%dirty = %dirty or 0b0110
@@ -647,7 +704,8 @@
%dirty = %dirty and 0b01110000.inv()
}
}
- println()
+ used(modifier)
+ content(%composer, 0b1110 and %dirty shr 0b0011)
} else {
%composer.skipToGroupEnd()
}
@@ -733,7 +791,7 @@
"""
""",
"""
- @Composable fun SomeThing(content: @Composable() () -> Unit) {}
+ @Composable fun SomeThing(content: @Composable() () -> Unit) { content() }
@Composable
fun Example() {
@@ -745,12 +803,13 @@
"""
@Composable
fun SomeThing(content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
- %composer = %composer.startRestartGroup(<>, "C(SomeThing):Test.kt")
+ %composer = %composer.startRestartGroup(<>, "C(SomeThing)<conten...>:Test.kt")
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
}
if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ content(%composer, 0b1110 and %dirty)
} else {
%composer.skipToGroupEnd()
}
@@ -904,9 +963,7 @@
@Composable
fun A(%composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(A):Test.kt")
- val %dirty = %changed
- %dirty = %dirty or 0b0110
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b0001 !== 0 || !%composer.skipping) {
print("hello world")
} else {
%composer.skipToGroupEnd()
@@ -919,7 +976,6 @@
@Composable
fun B(%composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
- val %dirty = %changed
print(counter)
val tmp0_rcvr = <this>
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
@@ -939,7 +995,9 @@
"""
@Composable
fun Example(a: Int = 0, b: Int = makeInt(), c: Int = 0) {
-
+ used(a)
+ used(b)
+ used(c)
}
""",
"""
@@ -980,6 +1038,9 @@
%dirty = %dirty and 0b01110000.inv()
}
}
+ used(a)
+ used(b)
+ used(c)
} else {
%composer.skipToGroupEnd()
}
@@ -1001,7 +1062,9 @@
}
@Composable
fun Test(x: Int = 0, y: Int = 0) {
+ used(y)
Wrap(10) {
+ used(it)
A(x)
}
}
@@ -1047,12 +1110,14 @@
if (%default and 0b0010 !== 0) {
y = 0
}
+ used(y)
Wrap(10, composableLambda(%composer, <>, true, "C<A(x)>:Test.kt") { it: Int, %composer: Composer?, %changed: Int ->
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
}
if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ used(it)
A(x, 0, %composer, 0b1110 and %dirty, 0b0010)
} else {
%composer.skipToGroupEnd()
@@ -1186,9 +1251,12 @@
""",
"""
@Composable fun CanSkip(a: Int = 0, b: Foo = Foo()) {
- print("Hello World")
+ used(a)
+ used(b)
}
@Composable fun CannotSkip(a: Int, b: Foo) {
+ used(a)
+ used(b)
print("Hello World")
}
@Composable fun NoParams() {
@@ -1226,7 +1294,8 @@
%dirty = %dirty and 0b01110000.inv()
}
}
- print("Hello World")
+ used(a)
+ used(b)
} else {
%composer.skipToGroupEnd()
}
@@ -1237,6 +1306,8 @@
@Composable
fun CannotSkip(a: Int, b: Foo, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>, "C(CannotSkip):Test.kt")
+ used(a)
+ used(b)
print("Hello World")
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
CannotSkip(a, b, %composer, %changed or 0b0001)
@@ -1258,6 +1329,48 @@
)
@Test
+ fun testOptionalUnstableWithStableExtensionReceiver(): Unit = comparisonPropagation(
+ """
+ class Foo(var value: Int = 0)
+ class Bar
+ """,
+ """
+ @Composable fun Bar.CanSkip(b: Foo = Foo()) {
+ print("Hello World")
+ }
+ """,
+ """
+ @Composable
+ fun Bar.CanSkip(b: Foo?, %composer: Composer?, %changed: Int, %default: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(CanSkip):Test.kt")
+ val %dirty = %changed
+ if (%default.inv() and 0b0001 !== 0 || %dirty and 0b0001 !== 0 || !%composer.skipping) {
+ if (%changed and 0b0001 === 0 || %composer.defaultsInvalid) {
+ %composer.startDefaults()
+ if (%default and 0b0001 !== 0) {
+ b = Foo(
+ )
+ %dirty = %dirty and 0b01110000.inv()
+ }
+ %composer.endDefaults()
+ } else {
+ %composer.skipCurrentGroup()
+ if (%default and 0b0001 !== 0) {
+ %dirty = %dirty and 0b01110000.inv()
+ }
+ }
+ print("Hello World")
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ CanSkip(b, %composer, %changed or 0b0001, %default)
+ }
+ }
+ """
+ )
+
+ @Test
fun testNoParams(): Unit = comparisonPropagation(
"""
@Composable fun A() {}
@@ -1331,7 +1444,10 @@
}
@Composable
- fun B(text: String, color: Color = Color.Unset) {}
+ fun B(text: String, color: Color = Color.Unset) {
+ used(text)
+ used(color)
+ }
""",
"""
@Composable
@@ -1376,6 +1492,8 @@
%dirty = %dirty and 0b01110000.inv()
}
}
+ used(text)
+ used(color)
} else {
%composer.skipToGroupEnd()
}
@@ -1785,6 +1903,151 @@
)
@Test
+ fun testLambdaSkipping(): Unit = comparisonPropagation(
+ """
+ import androidx.compose.runtime.*
+
+ data class User(
+ val id: Int,
+ val name: String
+ )
+
+ interface LazyPagingItems<T> {
+ val itemCount: Int
+ operator fun get(index: Int): State<T?>
+ }
+
+ @Stable interface LazyListScope {
+ fun items(itemCount: Int, itemContent: @Composable LazyItemScope.(Int) -> Unit)
+ }
+
+ @Stable interface LazyItemScope
+
+ public fun <T : Any> LazyListScope.itemsIndexed(
+ lazyPagingItems: LazyPagingItems<T>,
+ itemContent: @Composable LazyItemScope.(Int, T?) -> Unit
+ ) {
+ items(lazyPagingItems.itemCount) { index ->
+ val item = lazyPagingItems[index].value
+ itemContent(index, item)
+ }
+ }
+ """,
+ """
+ fun LazyListScope.Example(items: LazyPagingItems<User>) {
+ itemsIndexed(items) { index, user ->
+ print("Hello World")
+ }
+ }
+ """,
+ """
+ fun LazyListScope.Example(items: LazyPagingItems<User>) {
+ itemsIndexed(items, ComposableSingletons%TestKt.lambda-1)
+ }
+ internal object ComposableSingletons%TestKt {
+ val lambda-1: @[ExtensionFunctionType] Function5<LazyItemScope, Int, User?, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:Test.kt") { index: Int, user: User?, %composer: Composer?, %changed: Int ->
+ if (%changed and 0b0001010000000001 xor 0b010000000000 !== 0 || !%composer.skipping) {
+ print("Hello World")
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testPassedExtensionWhenExtensionIsPotentiallyUnstable(): Unit = comparisonPropagation(
+ """
+ interface Unstable
+ """,
+ """
+ @Composable fun Unstable.Test() {
+ doSomething(this) // does this reference %dirty without %dirty
+ }
+
+ @Composable fun doSomething(x: Unstable) {}
+ """,
+ """
+ @Composable
+ fun Unstable.Test(%composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Test)<doSome...>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ doSomething(<this>, %composer, 0b1110 and %dirty)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(%composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun doSomething(x: Unstable, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(doSomething):Test.kt")
+ if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ doSomething(x, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testReceiverIssue(): Unit = comparisonPropagation(
+ """
+ class Foo
+ """,
+ """
+ import androidx.compose.runtime.ExplicitGroupsComposable
+
+ @Composable
+ @ExplicitGroupsComposable
+ fun A(foo: Foo) {
+ foo.b()
+ }
+
+ @Composable
+ @ExplicitGroupsComposable
+ inline fun Foo.b(label: String = "") {
+ c(this, label)
+ }
+
+ @Composable
+ @ExplicitGroupsComposable
+ inline fun c(foo: Foo, label: String) {
+ print(label)
+ }
+ """,
+ """
+ @Composable
+ @ExplicitGroupsComposable
+ fun A(foo: Foo, %composer: Composer?, %changed: Int) {
+ foo.b(null, %composer, 0b1110 and %changed, 0b0001)
+ }
+ @Composable
+ @ExplicitGroupsComposable
+ fun Foo.b(label: String?, %composer: Composer?, %changed: Int, %default: Int) {
+ if (%default and 0b0001 !== 0) {
+ label = ""
+ }
+ c(<this>, label, %composer, 0b1110 and %changed or 0b01110000 and %changed)
+ }
+ @Composable
+ @ExplicitGroupsComposable
+ fun c(foo: Foo, label: String, %composer: Composer?, %changed: Int) {
+ print(label)
+ }
+ """
+ )
+
+ @Test
fun testDifferentParameters(): Unit = comparisonPropagation(
"""
@Composable fun B(a: Int, b: Int, c: Int, d: Int) {}
@@ -1838,12 +2101,12 @@
val unstableUnused: @Composable Foo.() -> Unit = {
}
val unstableUsed: @Composable Foo.() -> Unit = {
- print(x)
+ used(x)
}
val stableUnused: @Composable StableFoo.() -> Unit = {
}
val stableUsed: @Composable StableFoo.() -> Unit = {
- print(x)
+ used(x)
}
""",
"""
@@ -1853,9 +2116,7 @@
val stableUsed: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-4
internal object ComposableSingletons%TestKt {
val lambda-1: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
- val %dirty = %changed
- %dirty = %dirty or 0b0110
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -1863,12 +2124,17 @@
}
val lambda-2: @[ExtensionFunctionType] Function3<Foo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
val %dirty = %changed
- print(%this%null.x)
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ used(%this%null.x)
+ } else {
+ %composer.skipToGroupEnd()
+ }
}
val lambda-3: @[ExtensionFunctionType] Function3<StableFoo, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { %composer: Composer?, %changed: Int ->
- val %dirty = %changed
- %dirty = %dirty or 0b0110
- if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ if (%changed and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
Unit
} else {
%composer.skipToGroupEnd()
@@ -1880,7 +2146,7 @@
%dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
}
if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
- print(%this%null.x)
+ used(%this%null.x)
} else {
%composer.skipToGroupEnd()
}
@@ -1974,19 +2240,9 @@
if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
@Composable
fun foo(y: Int, %composer: Composer?, %changed: Int) {
- %composer = %composer.startRestartGroup(<>, "C(foo)<B(x,>:Test.kt")
- val %dirty = %changed
- if (%changed and 0b1110 === 0) {
- %dirty = %dirty or if (%composer.changed(y)) 0b0100 else 0b0010
- }
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
- B(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %dirty shl 0b0011)
- } else {
- %composer.skipToGroupEnd()
- }
- %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
- foo(y, %composer, %changed or 0b0001)
- }
+ %composer.startReplaceableGroup(<>, "C(foo)<B(x,>:Test.kt")
+ B(x, y, %composer, 0b1110 and %dirty or 0b01110000 and %changed shl 0b0011)
+ %composer.endReplaceableGroup()
}
foo(x, %composer, 0b1110 and %dirty)
} else {
@@ -2580,13 +2836,15 @@
paddingStart: Dp = Dp.Unspecified,
content: @Composable () -> Unit = {}
) {
-
+ used(modifier)
+ used(paddingStart)
+ content()
}
""",
"""
@Composable
fun Box2(modifier: Modifier?, paddingStart: Dp, content: Function2<Composer, Int, Unit>?, %composer: Composer?, %changed: Int, %default: Int) {
- %composer = %composer.startRestartGroup(<>, "C(Box2)P(1,2:c#ui.unit.Dp):Test.kt")
+ %composer = %composer.startRestartGroup(<>, "C(Box2)P(1,2:c#ui.unit.Dp)<conten...>:Test.kt")
val %dirty = %changed
if (%default and 0b0001 !== 0) {
%dirty = %dirty or 0b0110
@@ -2623,6 +2881,9 @@
%dirty = %dirty and 0b001110000000.inv()
}
}
+ used(modifier)
+ used(paddingStart)
+ content(%composer, 0b1110 and %dirty shr 0b0110)
} else {
%composer.skipToGroupEnd()
}
@@ -2693,4 +2954,139 @@
}
"""
)
+
+ @Test
+ fun testUnusedParameters(): Unit = comparisonPropagation(
+ """
+ class Unstable(var count: Int)
+ class Stable(val count: Int)
+ interface MaybeStable
+ """,
+ """
+ @Composable
+ fun Unskippable(a: Unstable, b: Stable, c: MaybeStable) {
+ used(a)
+ }
+ @Composable
+ fun Skippable1(a: Unstable, b: Stable, c: MaybeStable) {
+ used(b)
+ }
+ @Composable
+ fun Skippable2(a: Unstable, b: Stable, c: MaybeStable) {
+ used(c)
+ }
+ @Composable
+ fun Skippable3(a: Unstable, b: Stable, c: MaybeStable) { }
+ """,
+ """
+ @Composable
+ fun Unskippable(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Unskippable):Test.kt")
+ used(a)
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Unskippable(a, b, c, %composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun Skippable1(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Skippable1):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b01110000 === 0) {
+ %dirty = %dirty or if (%composer.changed(b)) 0b00100000 else 0b00010000
+ }
+ if (%dirty and 0b01010001 xor 0b00010000 !== 0 || !%composer.skipping) {
+ used(b)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Skippable1(a, b, c, %composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun Skippable2(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Skippable2):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b001110000000 === 0) {
+ %dirty = %dirty or if (%composer.changed(c)) 0b000100000000 else 0b10000000
+ }
+ if (%dirty and 0b001010000001 xor 0b10000000 !== 0 || !%composer.skipping) {
+ used(c)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Skippable2(a, b, c, %composer, %changed or 0b0001)
+ }
+ }
+ @Composable
+ fun Skippable3(a: Unstable, b: Stable, c: MaybeStable, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(Skippable3):Test.kt")
+ if (%changed and 0b0001 !== 0 || !%composer.skipping) {
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Skippable3(a, b, c, %composer, %changed or 0b0001)
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testExtensionReceiver(): Unit = comparisonPropagation(
+ """
+ interface MaybeStable
+ """,
+ """
+ @Composable fun MaybeStable.example(x: Int) {
+ used(this)
+ used(x)
+ }
+ val example: @Composable MaybeStable.(Int) -> Unit = {
+ used(this)
+ used(it)
+ }
+ """,
+ """
+ @Composable
+ fun MaybeStable.example(x: Int, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>, "C(example):Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(<this>)) 0b0100 else 0b0010
+ }
+ if (%changed and 0b01110000 === 0) {
+ %dirty = %dirty or if (%composer.changed(x)) 0b00100000 else 0b00010000
+ }
+ if (%dirty and 0b01011011 xor 0b00010010 !== 0 || !%composer.skipping) {
+ used(<this>)
+ used(x)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ example(x, %composer, %changed or 0b0001)
+ }
+ }
+ val example: @[ExtensionFunctionType] Function4<MaybeStable, Int, Composer, Int, Unit> = ComposableSingletons%TestKt.lambda-1
+ internal object ComposableSingletons%TestKt {
+ val lambda-1: @[ExtensionFunctionType] Function4<MaybeStable, Int, Composer, Int, Unit> = composableLambdaInstance(<>, false, "C:") { it: Int, %composer: Composer?, %changed: Int ->
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(%this%null)) 0b0100 else 0b0010
+ }
+ if (%changed and 0b01110000 === 0) {
+ %dirty = %dirty or if (%composer.changed(it)) 0b00100000 else 0b00010000
+ }
+ if (%dirty and 0b001011011011 xor 0b10010010 !== 0 || !%composer.skipping) {
+ used(%this%null)
+ used(it)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ }
+ """
+ )
}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 7e64583..838f9bf 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -57,7 +57,7 @@
@Composable fun Example() {
@Composable fun A() { }
- @Composable fun B(content: @Composable () -> Unit) { }
+ @Composable fun B(content: @Composable () -> Unit) { content() }
@Composable fun C() {
B { A() }
}
@@ -70,47 +70,26 @@
if (%changed !== 0 || !%composer.skipping) {
@Composable
fun A(%composer: Composer?, %changed: Int) {
- %composer = %composer.startRestartGroup(<>, "C(A):Test.kt")
- if (%changed !== 0 || !%composer.skipping) {
- } else {
- %composer.skipToGroupEnd()
- }
- %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
- A(%composer, %changed or 0b0001)
- }
+ %composer.startReplaceableGroup(<>, "C(A):Test.kt")
+ %composer.endReplaceableGroup()
}
@Composable
fun B(content: Function2<Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
- %composer = %composer.startRestartGroup(<>, "C(B):Test.kt")
- val %dirty = %changed
- if (%changed and 0b1110 === 0) {
- %dirty = %dirty or if (%composer.changed(content)) 0b0100 else 0b0010
- }
- if (%dirty and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
- } else {
- %composer.skipToGroupEnd()
- }
- %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
- B(content, %composer, %changed or 0b0001)
- }
+ %composer.startReplaceableGroup(<>, "C(B)<conten...>:Test.kt")
+ content(%composer, 0b1110 and %changed)
+ %composer.endReplaceableGroup()
}
@Composable
fun C(%composer: Composer?, %changed: Int) {
- %composer = %composer.startRestartGroup(<>, "C(C)<B>:Test.kt")
- if (%changed !== 0 || !%composer.skipping) {
- B(composableLambda(%composer, <>, false, "C<A()>:Test.kt") { %composer: Composer?, %changed: Int ->
- if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
- A(%composer, 0)
- } else {
- %composer.skipToGroupEnd()
- }
- }, %composer, 0b0110)
- } else {
- %composer.skipToGroupEnd()
- }
- %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
- C(%composer, %changed or 0b0001)
- }
+ %composer.startReplaceableGroup(<>, "C(C)<B>:Test.kt")
+ B(composableLambda(%composer, <>, false, "C<A()>:Test.kt") { %composer: Composer?, %changed: Int ->
+ if (%changed and 0b1011 xor 0b0010 !== 0 || !%composer.skipping) {
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }, %composer, 0b0110)
+ %composer.endReplaceableGroup()
}
} else {
%composer.skipToGroupEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
index 6798cf6..6fcf50c 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
@@ -597,7 +597,7 @@
// since the lowering will throw an exception if duplicate keys are found, all we have to do
// is run the lowering
private fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
- generateIrModuleWithJvmResolve(
+ JvmCompilation().compile(
listOf(
sourceFile("Test.kt", src.replace('%', '$'))
)
@@ -607,7 +607,7 @@
// For a given src string, a
private fun assertKeys(vararg keys: String, makeSrc: () -> String) {
builtKeys = mutableSetOf()
- generateIrModuleWithJvmResolve(
+ JvmCompilation().compile(
listOf(
sourceFile("Test.kt", makeSrc().replace('%', '$'))
)
@@ -624,7 +624,7 @@
// test: have two src strings (before/after) and assert that the keys of the params didn't change
private fun assertDurableChange(before: String, after: String) {
- generateIrModuleWithJvmResolve(
+ JvmCompilation().compile(
listOf(
sourceFile("Test.kt", before.replace('%', '$'))
)
@@ -633,7 +633,7 @@
builtKeys = mutableSetOf()
- generateIrModuleWithJvmResolve(
+ JvmCompilation().compile(
listOf(
sourceFile("Test.kt", after.replace('%', '$'))
)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 9d46a28..51532ad 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -17,6 +17,7 @@
package androidx.compose.compiler.plugins.kotlin
import org.intellij.lang.annotations.Language
+import org.junit.Ignore
import org.junit.Test
class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
@@ -39,6 +40,7 @@
import androidx.compose.runtime.Composable
$unchecked
+ fun used(x: Any?) {}
""".trimIndent(),
dumpTree = dumpTree
)
@@ -666,6 +668,7 @@
@Composable
fun Test(items: List<Int>) {
val foo = remember { Foo() }
+ used(items)
}
""",
"""
@@ -676,6 +679,7 @@
val tmp0_return = Foo()
tmp0_return
}
+ used(items)
%composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
Test(items, %composer, %changed or 0b0001)
}
@@ -900,8 +904,8 @@
"""
)
- @Test
- fun testOptimizationFailsIfDefaultsGroupIsUsed(): Unit = comparisonPropagation(
+ @Ignore("This test must pass before intrinsic remember can be turned on")
+ fun xtestOptimizationFailsIfDefaultsGroupIsUsed(): Unit = comparisonPropagation(
"""
class Foo
fun someInt(): Int = 123
@@ -910,8 +914,8 @@
@Composable
fun Test(a: Int = someInt()) {
val foo = remember { Foo() }
- print(foo)
- print(a)
+ used(foo)
+ used(a)
}
""",
"""
@@ -940,8 +944,8 @@
val tmp0_return = Foo()
tmp0_return
}, %composer, 0)
- print(foo)
- print(a)
+ used(foo)
+ used(a)
} else {
%composer.skipToGroupEnd()
}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
index 024f822..9023705 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RobolectricComposeTester.kt
@@ -28,7 +28,6 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import org.robolectric.Robolectric
@@ -115,7 +114,6 @@
}
companion object {
- @OptIn(ExperimentalCoroutinesApi::class)
private val recomposer = run {
val mainScope = CoroutineScope(
NonCancellable + Dispatchers.Main
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 49c690a..09d2ab6 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -127,6 +127,7 @@
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.util.ConstantValueGenerator
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
import org.jetbrains.kotlin.ir.util.TypeTranslator
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.getArguments
@@ -920,17 +921,19 @@
isVar: Boolean = false,
origin: IrDeclarationOrigin = IrDeclarationOrigin.IR_TEMPORARY_VARIABLE
): IrVariableImpl {
+ val descriptor = WrappedVariableDescriptor()
return IrVariableImpl(
value.startOffset,
value.endOffset,
origin,
- IrVariableSymbolImpl(WrappedVariableDescriptor()),
+ IrVariableSymbolImpl(descriptor),
Name.identifier(name),
irType,
isVar,
false,
false
).apply {
+ descriptor.bind(this)
initializer = value
}
}
@@ -1044,6 +1047,8 @@
fun makeStabilityField(): IrField {
return context.irFactory.buildField {
+ startOffset = SYNTHETIC_OFFSET
+ endOffset = SYNTHETIC_OFFSET
name = KtxNameConventions.STABILITY_FLAG
isStatic = true
isFinal = true
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
index a872344..4c86015 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableFunctionBodyTransformer.kt
@@ -29,6 +29,7 @@
import org.jetbrains.kotlin.backend.common.lower.DeclarationIrBuilder
import org.jetbrains.kotlin.backend.common.pop
import org.jetbrains.kotlin.backend.common.push
+import org.jetbrains.kotlin.backend.jvm.JvmLoweredDeclarationOrigin
import org.jetbrains.kotlin.builtins.PrimitiveType
import org.jetbrains.kotlin.builtins.StandardNames
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
@@ -136,7 +137,9 @@
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.getPropertyGetter
import org.jetbrains.kotlin.ir.util.isInlined
+import org.jetbrains.kotlin.ir.util.isLocal
import org.jetbrains.kotlin.ir.util.isVararg
+import org.jetbrains.kotlin.ir.util.parentClassOrNull
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.ir.util.properties
import org.jetbrains.kotlin.ir.util.statements
@@ -251,10 +254,11 @@
}
interface IrChangedBitMaskValue {
+ val used: Boolean
fun irLowBit(): IrExpression
fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression
fun irSlotAnd(slot: Int, bits: Int): IrExpression
- fun irHasDifferences(): IrExpression
+ fun irHasDifferences(usedParams: BooleanArray): IrExpression
fun irCopyToTemporary(
nameHint: String? = null,
isVar: Boolean = false,
@@ -721,6 +725,10 @@
if (body == null)
return false
+ if (isLocal && parentClassOrNull?.origin != JvmLoweredDeclarationOrigin.LAMBDA_IMPL) {
+ return false
+ }
+
val descriptor = descriptor
// Do not insert observe scope in an inline function
@@ -730,6 +738,9 @@
if (descriptor.hasNonRestartableComposableAnnotation())
return false
+ if (descriptor.hasExplicitGroupsAnnotation())
+ return false
+
// Do not insert an observe scope in an inline composable lambda
descriptor.findPsi()?.let { psi ->
(psi as? KtFunctionLiteral)?.let {
@@ -797,31 +808,24 @@
scope.dirty = changedParam
- val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
- buildStatementsForSkippingAndDefaults(
- body,
- skipPreamble,
- bodyPreamble,
- false,
- realParams,
- scope,
- changedParam,
- changedParam,
- defaultParam,
- booleanArrayOf()
- )
-
- realParams.forEach {
- // we want to remove the default expression from the function. This will prevent
- // the kotlin compiler from doing its own default handling, which we don't need.
- it.defaultValue = null
- }
+ val defaultScope = transformDefaults(scope)
var (transformed, returnVar) = body.asBodyAndResultVar()
transformed = transformed.apply { transformChildrenVoid() }
+ buildPreambleStatementsAndReturnIfSkippingPossible(
+ body,
+ skipPreamble,
+ bodyPreamble,
+ false,
+ scope,
+ changedParam,
+ changedParam,
+ defaultParam,
+ defaultScope,
+ )
+
if (!elideGroups) scope.realizeGroup(::irEndReplaceableGroup)
declaration.body = IrBlockBodyImpl(
@@ -836,7 +840,6 @@
)
else
null,
- *skipPreamble.statements.toTypedArray(),
*bodyPreamble.statements.toTypedArray(),
*transformed.statements.toTypedArray(),
if (!elideGroups) irEndReplaceableGroup() else null,
@@ -865,24 +868,14 @@
val skipPreamble = mutableStatementContainer()
val bodyPreamble = mutableStatementContainer()
- val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
- val realParamsIncludingThis = realParams + listOfNotNull(
- declaration.extensionReceiverParameter
- )
-
- // boolean array mapped to parameters. true indicates that the type is unstable
- val unstableMask = realParams.map {
- stabilityOf(it.type).knownUnstable()
- }.toBooleanArray()
-
// we start off assuming that we *can* skip execution of the function
- var canSkipExecution = unstableMask.none { it } && declaration.returnType.isUnit()
+ var canSkipExecution = declaration.returnType.isUnit() &&
+ scope.allTrackedParams.none { stabilityOf(it.type).knownUnstable() }
// if the function can never skip, or there are no parameters to test, then we
// don't need to have the dirty parameter locally since it will never be different from
// the passed in `changed` parameter.
- val dirty = if (canSkipExecution && realParamsIncludingThis.isNotEmpty())
+ val dirty = if (canSkipExecution && scope.allTrackedParams.isNotEmpty())
// NOTE(lmr): Technically, dirty is a mutable variable, but we don't want to mark it
// as one since that will cause a `Ref<Int>` to get created if it is captured. Since
// we know we will never be mutating this variable _after_ it gets captured, we can
@@ -891,51 +884,40 @@
isVar = false,
nameHint = "\$dirty",
exactName = true
- ).also {
- skipPreamble.statements.addAll(it.asStatements())
- }
+ )
else
changedParam
scope.dirty = dirty
- buildStatementsForSkippingAndDefaults(
- body,
- skipPreamble,
- bodyPreamble,
- canSkipExecution,
- realParams,
- scope,
- dirty,
- changedParam,
- null,
- unstableMask
- )
-
val (nonReturningBody, returnVar) = body.asBodyAndResultVar()
// we must transform the body first, since that will allow us to see whether or not we
// are using the dispatchReceiverParameter or the extensionReceiverParameter
val transformed = nonReturningBody.apply { transformChildrenVoid() }
- if (declaration.extensionReceiverParameter != null) {
- canSkipExecution = buildStatementsForSkippingThisParameter(
- declaration.extensionReceiverParameter!!,
- scope.extensionReceiverUsed,
- canSkipExecution,
- skipPreamble,
- changedParam,
- dirty,
- scope.realValueParamCount
- )
- }
+ canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
+ body,
+ skipPreamble,
+ bodyPreamble,
+ canSkipExecution,
+ scope,
+ dirty,
+ changedParam,
+ null,
+ Scope.ParametersScope(),
+ )
+ val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) {
+ skipPreamble.statements.addAll(0, dirty.asStatements())
+ dirty
+ } else changedParam
if (canSkipExecution) {
// We CANNOT skip if any of the following conditions are met
// 1. if any of the stable parameters have *differences* from last execution.
// 2. if the composer.skipping call returns false
val shouldExecute = irOrOr(
- scope.dirty!!.irHasDifferences(),
+ dirtyForSkipping.irHasDifferences(scope.usedParams),
irNot(irIsSkipping())
)
@@ -998,75 +980,24 @@
val skipPreamble = mutableStatementContainer()
val bodyPreamble = mutableStatementContainer()
- // these are the parameters excluding the synthetic ones that we generate for compose.
- // These are the only parameters we want to consider in skipping calculations
- val realParams = declaration.valueParameters.take(scope.realValueParamCount)
-
- val thisParams = listOfNotNull(
- declaration.extensionReceiverParameter,
- declaration.dispatchReceiverParameter
- )
-
- val realParamsIncludingThis = realParams + thisParams
-
// we start off assuming that we *can* skip execution of the function
var canSkipExecution = true
- // boolean array mapped to parameters. true indicates that the type is unstable
- val unstableMask = realParams.map {
- if (stabilityOf((it.varargElementType ?: it.type)).knownUnstable()) {
- if (!it.hasDefaultValueSafe()) {
- // if it has non-optional unstable params, the function can never skip
- canSkipExecution = false
- }
- true
- } else {
- false
- }
- }.toBooleanArray()
-
- // if the function can never skip, or there are no parameters to test, then we
- // don't need to have the dirty parameter locally since it will never be different from
- // the passed in `changed` parameter.
- val dirty = if (canSkipExecution && realParamsIncludingThis.isNotEmpty())
// NOTE(lmr): Technically, dirty is a mutable variable, but we don't want to mark it
// as one since that will cause a `Ref<Int>` to get created if it is captured. Since
// we know we will never be mutating this variable _after_ it gets captured, we can
// safely mark this as `isVar = false`.
+ val dirty = if (scope.allTrackedParams.isNotEmpty())
changedParam.irCopyToTemporary(
isVar = false,
nameHint = "\$dirty",
exactName = true
- ).also {
- skipPreamble.statements.addAll(it.asStatements())
- }
+ )
else
changedParam
scope.dirty = dirty
- buildStatementsForSkippingAndDefaults(
- body,
- skipPreamble,
- bodyPreamble,
- canSkipExecution,
- realParams,
- scope,
- dirty,
- changedParam,
- defaultParam,
- unstableMask
- )
-
- realParams.forEach {
- // we want to remove the default expression from the function. This will prevent
- // the kotlin compiler from doing its own default handling, which we don't need.
-
- // NOTE: we are doing this AFTER buildStatementsForSkipping, because the default
- // value is used in those calculations
- it.defaultValue = null
- }
-
val (nonReturningBody, returnVar) = body.asBodyAndResultVar()
val end = {
@@ -1078,39 +1009,31 @@
)
}
+ val defaultScope = transformDefaults(scope)
+
// we must transform the body first, since that will allow us to see whether or not we
// are using the dispatchReceiverParameter or the extensionReceiverParameter
val transformed = nonReturningBody.apply { transformChildrenVoid() }
- var slotIndex = scope.realValueParamCount
-
- if (declaration.extensionReceiverParameter != null) {
- canSkipExecution = buildStatementsForSkippingThisParameter(
- declaration.extensionReceiverParameter!!,
- scope.extensionReceiverUsed,
- canSkipExecution,
- skipPreamble,
- changedParam,
- dirty,
- slotIndex++
- )
- }
-
- if (declaration.dispatchReceiverParameter != null) {
- canSkipExecution = buildStatementsForSkippingThisParameter(
- declaration.dispatchReceiverParameter!!,
- scope.dispatchReceiverUsed,
- canSkipExecution,
- skipPreamble,
- changedParam,
- dirty,
- slotIndex
- )
- }
+ canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible(
+ body,
+ skipPreamble,
+ bodyPreamble,
+ canSkipExecution,
+ scope,
+ dirty,
+ changedParam,
+ defaultParam,
+ defaultScope,
+ )
// if it has non-optional unstable params, the function can never skip, so we always
// execute the body. Otherwise, we wrap the body in an if and only skip when certain
// conditions are met.
+ val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) {
+ skipPreamble.statements.addAll(0, dirty.asStatements())
+ dirty
+ } else changedParam
val transformedBody = if (canSkipExecution) {
// We CANNOT skip if any of the following conditions are met
// 1. if any of the stable parameters have *differences* from last execution.
@@ -1119,11 +1042,20 @@
// (3) is only necessary to check if we actually have unstable params, so we only
// generate that check if we need to.
-
var shouldExecute = irOrOr(
- scope.dirty!!.irHasDifferences(),
+ dirtyForSkipping.irHasDifferences(scope.usedParams),
irNot(irIsSkipping())
)
+
+ // boolean array mapped to parameters. true indicates that the type is unstable
+ // NOTE: the unstable mask is indexed by valueParameter index, which is different
+ // than the slotIndex but that is OKAY because we only care about defaults, which
+ // also use the value parameter index.
+ val realParams = declaration.valueParameters.take(scope.realValueParamCount)
+ val unstableMask = realParams.map {
+ stabilityOf((it.varargElementType ?: it.type)).knownUnstable()
+ }.toBooleanArray()
+
val hasAnyUnstableParams = unstableMask.any { it }
// if there are unstable params, then we fence the whole expression with a check to
@@ -1195,228 +1127,202 @@
sourceFixups.clear()
}
- @ObsoleteDescriptorBasedAPI
- private fun buildStatementsForSkippingThisParameter(
- thisParam: IrValueParameter,
- isUsed: Boolean,
- canSkipExecution: Boolean,
- preamble: IrStatementContainer,
- changedParam: IrChangedBitMaskValue,
- dirty: IrChangedBitMaskValue,
- index: Int
- ): Boolean {
- val type = thisParam.type
- val isStable = stabilityOf(type).knownStable()
-
- return when {
- !isStable && isUsed -> false
- isStable && isUsed && canSkipExecution && dirty is IrChangedBitMaskVariable -> {
- preamble.statements.add(
- irIf(
- // we only call `$composer.changed(...)` on a parameter if the value came in
- // with an "Uncertain" state AND the value was provided. This is safe to do
- // because this will remain true or false for *every* execution of the
- // function, so we will never get a slot table misalignment as a result.
- condition = irIsUncertainAndStable(changedParam, index),
- body = dirty.irOrSetBitsAtSlot(
- index,
- irIfThenElse(
- context.irBuiltIns.intType,
- irChanged(irGet(thisParam)),
- // if the value has changed, update the bits in the slot to be
- // "Different"
- thenPart = irConst(ParamState.Different.bitsForSlot(index)),
- // if the value has not changed, update the bits in the slot to
- // be "Same"
- elsePart = irConst(ParamState.Same.bitsForSlot(index))
- )
- )
- )
- )
- true
+ private fun transformDefaults(scope: Scope.FunctionScope): Scope.ParametersScope {
+ val parameters = scope.allTrackedParams
+ val parametersScope = Scope.ParametersScope()
+ parameters.forEach { param ->
+ val defaultValue = param.defaultValue
+ if (defaultValue != null) {
+ defaultValue.expression = inScope(parametersScope) {
+ defaultValue.expression.transform(this, null)
+ }
}
- !isUsed && canSkipExecution && dirty is IrChangedBitMaskVariable -> {
- // if the param isn't used we can safely ignore it, but if we can skip the
- // execution of the function, then we need to make sure that we are only
- // considering the not-ignored parameters. to do this, we set the changed slot bits
- // to Static
- preamble.statements.add(
- dirty.irOrSetBitsAtSlot(
- index,
- irConst(ParamState.Static.bitsForSlot(index))
- )
- )
- }
- // nothing changes
- else -> canSkipExecution
}
+ return parametersScope
}
@ObsoleteDescriptorBasedAPI
- private fun buildStatementsForSkippingAndDefaults(
+ private fun buildPreambleStatementsAndReturnIfSkippingPossible(
sourceElement: IrElement,
skipPreamble: IrStatementContainer,
bodyPreamble: IrStatementContainer,
- canSkipExecution: Boolean,
- parameters: List<IrValueParameter>,
+ isSkippableDeclaration: Boolean,
scope: Scope.FunctionScope,
dirty: IrChangedBitMaskValue,
changedParam: IrChangedBitMaskValue,
defaultParam: IrDefaultBitMaskValue?,
- unstableMask: BooleanArray
- ) {
+ defaultScope: Scope.ParametersScope
+ ): Boolean {
+ val parameters = scope.allTrackedParams
// we default to true because the absence of a default expression we want to consider as
// "static"
val defaultExprIsStatic = BooleanArray(parameters.size) { true }
val defaultExpr = Array<IrExpression?>(parameters.size) { null }
+ val stabilities = Array(parameters.size) { Stability.Unstable }
+ var mightSkip = isSkippableDeclaration
- // first we create the necessary local variables for default handling.
val setDefaults = mutableStatementContainer()
val skipDefaults = mutableStatementContainer()
- val parametersScope = Scope.ParametersScope()
- parameters.forEachIndexed { index, param ->
- val defaultValue = param.defaultValue
+// val parametersScope = Scope.ParametersScope()
+ parameters.forEachIndexed { slotIndex, param ->
+ val defaultIndex = scope.defaultIndexForSlotIndex(slotIndex)
+ val defaultValue = param.defaultValue?.expression
if (defaultParam != null && defaultValue != null) {
- val transformedDefault = inScope(parametersScope) {
- defaultValue.expression.transform(this, null)
- }
+// val transformedDefault = inScope(parametersScope) {
+// defaultValue.expression.transform(this, null)
+// }
// we want to call this on the transformed version.
- defaultExprIsStatic[index] = transformedDefault.isStatic()
- defaultExpr[index] = transformedDefault
-
- // Generate code to reassign parameter local for default arguments.
- if (
- canSkipExecution &&
- !defaultExprIsStatic[index] &&
- dirty is IrChangedBitMaskVariable
- ) {
- // If we are setting the parameter to the default expression and
- // running the default expression again, and the expression isn't
- // provably static, we can't be certain that the dirty value of
- // SAME is going to be valid. We must mark it as UNCERTAIN. In order
- // to avoid slot-table misalignment issues, we must mark it as
- // UNCERTAIN even when we skip the defaults, so that any child
- // function receives UNCERTAIN vs SAME/DIFFERENT deterministically.
- setDefaults.statements.add(
- irIf(
- condition = irGetBit(defaultParam, index),
- body = irBlock(
- statements = listOf(
- irSet(param, transformedDefault),
- dirty.irSetSlotUncertain(index)
+ defaultExprIsStatic[slotIndex] = defaultValue.isStatic()
+ defaultExpr[slotIndex] = defaultValue
+ val hasStaticDefaultExpr = defaultExprIsStatic[slotIndex]
+ when {
+ isSkippableDeclaration && !hasStaticDefaultExpr &&
+ dirty is IrChangedBitMaskVariable -> {
+ // If we are setting the parameter to the default expression and
+ // running the default expression again, and the expression isn't
+ // provably static, we can't be certain that the dirty value of
+ // SAME is going to be valid. We must mark it as UNCERTAIN. In order
+ // to avoid slot-table misalignment issues, we must mark it as
+ // UNCERTAIN even when we skip the defaults, so that any child
+ // function receives UNCERTAIN vs SAME/DIFFERENT deterministically.
+ setDefaults.statements.add(
+ irIf(
+ condition = irGetBit(defaultParam, defaultIndex),
+ body = irBlock(
+ statements = listOf(
+ irSet(param, defaultValue),
+ dirty.irSetSlotUncertain(slotIndex)
+ )
)
)
)
- )
- skipDefaults.statements.add(
- irIf(
- condition = irGetBit(defaultParam, index),
- body = dirty.irSetSlotUncertain(index)
+ skipDefaults.statements.add(
+ irIf(
+ condition = irGetBit(defaultParam, defaultIndex),
+ body = dirty.irSetSlotUncertain(slotIndex)
+ )
)
- )
- } else {
- setDefaults.statements.add(
- irIf(
- condition = irGetBit(defaultParam, index),
- body = irSet(param, transformedDefault)
+ }
+ else -> {
+ setDefaults.statements.add(
+ irIf(
+ condition = irGetBit(defaultParam, defaultIndex),
+ body = irSet(param, defaultValue)
+ )
)
- )
+ }
}
}
-
- // In order to propagate the change detection we might perform on this parameter,
- // we need to know which "slot" it is in
- scope.remappedParams[param] = param
- scope.paramsToSlots[param] = index
}
+
+ parameters.forEachIndexed { slotIndex, param ->
+ val stability = stabilityOf(param.varargElementType ?: param.type)
+
+ stabilities[slotIndex] = stability
+
+ val isRequired = param.defaultValue == null
+ val isUnstable = stability.knownUnstable()
+ val isUsed = scope.usedParams[slotIndex]
+
+ if (isUsed && isUnstable && isRequired) {
+ // if it is a used + unstable parameter with no default expression, the fn
+ // will _never_ skip
+ mightSkip = false
+ }
+ }
+
// we start the skipPreamble with all of the changed calls. These need to go at the top
// of the function's group. Note that these end up getting called *before* default
// expressions, but this is okay because it will only ever get called on parameters that
// are provided to the function
- parameters.forEachIndexed { index, param ->
+ parameters.forEachIndexed { slotIndex, param ->
// varargs get handled separately because they will require their own groups
if (param.isVararg) return@forEachIndexed
+ val defaultIndex = scope.defaultIndexForSlotIndex(slotIndex)
val defaultValue = param.defaultValue
- if (canSkipExecution && dirty is IrChangedBitMaskVariable) {
- if (unstableMask[index]) {
- if (defaultParam != null && defaultValue != null) {
- skipPreamble.statements.add(
- irIf(
- condition = irGetBit(defaultParam, index),
- body = dirty.irOrSetBitsAtSlot(
- index,
- irConst(ParamState.Same.bitsForSlot(index))
+ val isUnstable = stabilities[slotIndex].knownUnstable()
+ val isUsed = scope.usedParams[slotIndex]
+
+ when {
+ !mightSkip || !isUsed -> {
+ // nothing to do
+ }
+ dirty !is IrChangedBitMaskVariable -> {
+ // this will only ever be true when mightSkip is false, but we put this
+ // branch here so that `dirty` gets smart cast in later branches
+ }
+ isUnstable && defaultParam != null && defaultValue != null -> {
+ // if it has a default parameter then the function can still potentially skip
+ skipPreamble.statements.add(
+ irIf(
+ condition = irGetBit(defaultParam, defaultIndex),
+ body = dirty.irOrSetBitsAtSlot(
+ slotIndex,
+ irConst(ParamState.Same.bitsForSlot(slotIndex))
+ )
+ )
+ )
+ }
+ !isUnstable -> {
+ val defaultValueIsStatic = defaultExprIsStatic[slotIndex]
+ val callChanged = irChanged(irGet(param))
+ val isChanged = if (defaultParam != null && !defaultValueIsStatic)
+ irAndAnd(irIsProvided(defaultParam, slotIndex), callChanged)
+ else
+ callChanged
+ val modifyDirtyFromChangedResult = dirty.irOrSetBitsAtSlot(
+ slotIndex,
+ irIfThenElse(
+ context.irBuiltIns.intType,
+ isChanged,
+ // if the value has changed, update the bits in the slot to be
+ // "Different"
+ thenPart = irConst(ParamState.Different.bitsForSlot(slotIndex)),
+ // if the value has not changed, update the bits in the slot to
+ // be "Same"
+ elsePart = irConst(ParamState.Same.bitsForSlot(slotIndex))
+ )
+ )
+
+ val stmt = if (defaultParam != null && defaultValueIsStatic) {
+ // if the default expression is "static", then we know that if we are using the
+ // default expression, the parameter can be considered "static".
+ irWhen(
+ origin = IrStatementOrigin.IF,
+ branches = listOf(
+ irBranch(
+ condition = irGetBit(defaultParam, defaultIndex),
+ result = dirty.irOrSetBitsAtSlot(
+ slotIndex,
+ irConst(ParamState.Static.bitsForSlot(slotIndex))
+ )
+ ),
+ irBranch(
+ condition = irIsUncertainAndStable(changedParam, slotIndex),
+ result = modifyDirtyFromChangedResult
)
)
)
+ } else {
+ // we only call `$composer.changed(...)` on a parameter if the value came in
+ // with an "Uncertain" state AND the value was provided. This is safe to do
+ // because this will remain true or false for *every* execution of the
+ // function, so we will never get a slot table misalignment as a result.
+ irIf(
+ condition = irIsUncertainAndStable(changedParam, slotIndex),
+ body = modifyDirtyFromChangedResult
+ )
}
-
- // if the value is unstable, there is no reason for us to store it in the slot table
- return@forEachIndexed
+ skipPreamble.statements.add(stmt)
}
-
- val defaultValueIsStatic = defaultExprIsStatic[index]
- val callChanged = irChanged(irGet(scope.remappedParams[param]!!))
- val isChanged = if (defaultParam != null && !defaultValueIsStatic)
- irAndAnd(irIsProvided(defaultParam, index), callChanged)
- else
- callChanged
- val modifyDirtyFromChangedResult = dirty.irOrSetBitsAtSlot(
- index,
- irIfThenElse(
- context.irBuiltIns.intType,
- isChanged,
- // if the value has changed, update the bits in the slot to be
- // "Different"
- thenPart = irConst(ParamState.Different.bitsForSlot(index)),
- // if the value has not changed, update the bits in the slot to
- // be "Same"
- elsePart = irConst(ParamState.Same.bitsForSlot(index))
- )
- )
-
- val stmt = if (defaultParam != null && defaultValueIsStatic) {
- // if the default expression is "static", then we know that if we are using the
- // default expression, the parameter can be considered "static".
- irWhen(
- origin = IrStatementOrigin.IF,
- branches = listOf(
- irBranch(
- condition = irGetBit(defaultParam, index),
- result = dirty.irOrSetBitsAtSlot(
- index,
- irConst(ParamState.Static.bitsForSlot(index))
- )
- ),
- irBranch(
- condition = irIsUncertainAndStable(changedParam, index),
- result = modifyDirtyFromChangedResult
- )
- )
- )
- } else {
- // we only call `$composer.changed(...)` on a parameter if the value came in
- // with an "Uncertain" state AND the value was provided. This is safe to do
- // because this will remain true or false for *every* execution of the
- // function, so we will never get a slot table misalignment as a result.
- irIf(
- condition = irIsUncertainAndStable(changedParam, index),
- body = modifyDirtyFromChangedResult
- )
- }
- skipPreamble.statements.add(stmt)
}
}
- // now we handle the vararg parameters specially since it needs to create a group
- parameters.forEachIndexed { index, param ->
- val varargElementType = param.varargElementType ?: return@forEachIndexed
- if (canSkipExecution && dirty is IrChangedBitMaskVariable) {
- if (unstableMask[index]) {
- // if the value is unstable, there is no reason for us to store it in the slot table
- return@forEachIndexed
- }
+ // now we handle the vararg parameters specially since it needs to create a group
+ parameters.forEachIndexed { slotIndex, param ->
+ val varargElementType = param.varargElementType ?: return@forEachIndexed
+ if (mightSkip && dirty is IrChangedBitMaskVariable) {
// for vararg parameters of stable type, we can store each value in the slot
// table, but need to generate a group since the size of the array could change
// over time. In the future, we may want to make an optimization where whether or
@@ -1431,7 +1337,7 @@
skipPreamble.statements.add(
irStartReplaceableGroup(
param,
- parametersScope,
+ defaultScope,
irGetParamSize
)
)
@@ -1445,16 +1351,16 @@
irGet(param)
) { loopVar ->
dirty.irOrSetBitsAtSlot(
- index,
+ slotIndex,
irIfThenElse(
context.irBuiltIns.intType,
irChanged(irGet(loopVar)),
// if the value has changed, update the bits in the slot to be
// "Different".
- thenPart = irConst(ParamState.Different.bitsForSlot(index)),
+ thenPart = irConst(ParamState.Different.bitsForSlot(slotIndex)),
// if the value has not changed, we are still uncertain if the entire
// list of values has gone unchanged or not, so we use Uncertain
- elsePart = irConst(ParamState.Uncertain.bitsForSlot(index))
+ elsePart = irConst(ParamState.Uncertain.bitsForSlot(slotIndex))
)
)
}
@@ -1468,18 +1374,23 @@
// }
skipPreamble.statements.add(
irIf(
- condition = irIsUncertainAndStable(dirty, index),
+ condition = irIsUncertainAndStable(dirty, slotIndex),
body = dirty.irOrSetBitsAtSlot(
- index,
- irConst(ParamState.Same.bitsForSlot(index))
+ slotIndex,
+ irConst(ParamState.Same.bitsForSlot(slotIndex))
)
)
)
}
}
+ parameters.forEach {
+ // we want to remove the default expression from the function. This will prevent
+ // the kotlin compiler from doing its own default handling, which we don't need.
+ it.defaultValue = null
+ }
// after all of this, we need to potentially wrap the default setters in a group and if
// statement, to make sure that defaults are only executed when they need to be.
- if (!canSkipExecution || defaultExprIsStatic.all { it }) {
+ if (!mightSkip || defaultExprIsStatic.all { it }) {
// if we don't skip execution ever, then we don't need these groups at all.
// Additionally, if all of the defaults are static, we can avoid creating the groups
// as well.
@@ -1515,6 +1426,8 @@
)
)
}
+
+ return mightSkip
}
@OptIn(ObsoleteDescriptorBasedAPI::class)
@@ -1627,11 +1540,6 @@
context.irBuiltIns.intType
)
fn.body = localIrBuilder.irBlockBody {
-
- fun remappedParam(index: Int) = function.valueParameters[index].let {
- scope.remappedParams[it] ?: it
- }
-
// Call the function again with the same parameters
+irReturn(
irCall(function.symbol).apply {
@@ -1650,7 +1558,7 @@
IrSpreadElementImpl(
UNDEFINED_OFFSET,
UNDEFINED_OFFSET,
- irGet(remappedParam(index))
+ irGet(param)
)
)
)
@@ -1658,7 +1566,7 @@
} else {
// NOTE(lmr): should we be using the parameter here, or the temporary
// with the default value?
- putValueArgument(index, irGet(remappedParam(index)))
+ putValueArgument(index, irGet(param))
}
}
@@ -2366,15 +2274,15 @@
arg.isStatic() -> meta.isStatic = true
arg is IrGetValue -> {
val owner = arg.symbol.owner
- val found = extractParamMetaFromScopes(meta, owner)
- if (!found) {
- when (owner) {
- is IrVariable -> {
- if (owner.isConst) {
- meta.isStatic = true
- } else if (!owner.isVar && owner.initializer != null) {
- populateParamMeta(owner.initializer!!, meta)
- }
+ when (owner) {
+ is IrValueParameter -> {
+ extractParamMetaFromScopes(meta, owner)
+ }
+ is IrVariable -> {
+ if (owner.isConst) {
+ meta.isStatic = true
+ } else if (!owner.isVar && owner.initializer != null) {
+ populateParamMeta(owner.initializer!!, meta)
}
}
}
@@ -2670,8 +2578,12 @@
arg is IrVararg -> {
inputArgs.addAll(
arg.elements.mapNotNull {
- if (it is IrSpreadElement) hasSpreadArgs = true
- it as? IrExpression
+ if (it is IrSpreadElement) {
+ hasSpreadArgs = true
+ arg
+ } else {
+ it as? IrExpression
+ }
}
)
}
@@ -2873,13 +2785,19 @@
private fun extractParamMetaFromScopes(meta: ParamMeta, param: IrValueDeclaration): Boolean {
var scope: Scope? = currentScope
+ val fn = param.parent
while (scope != null) {
when (scope) {
is Scope.FunctionScope -> {
- if (scope.remappedParams.containsValue(param)) {
- meta.isCertain = true
- meta.maskParam = scope.dirty
- meta.maskSlot = scope.paramsToSlots[param]!!
+ if (scope.function == fn) {
+ if (scope.isComposable) {
+ val slotIndex = scope.allTrackedParams.indexOf(param)
+ if (slotIndex != -1) {
+ meta.isCertain = true
+ meta.maskParam = scope.dirty
+ meta.maskSlot = slotIndex
+ }
+ }
return true
}
}
@@ -2894,10 +2812,9 @@
extensionParam: ParamMeta?,
dispatchParam: ParamMeta?
): List<IrExpression> {
- val thisParams = listOfNotNull(extensionParam, dispatchParam)
- val allParams = valueParams + thisParams
+ val allParams = listOfNotNull(extensionParam) + valueParams + listOfNotNull(dispatchParam)
// passing in 0 for thisParams since they should be included in the params list
- val changedCount = changedParamCount(valueParams.size, thisParams.size)
+ val changedCount = changedParamCount(valueParams.size, allParams.size - valueParams.size)
val result = mutableListOf<IrExpression>()
for (i in 0 until changedCount) {
val start = i * SLOTS_PER_INT
@@ -2926,7 +2843,7 @@
// (the shift amount represented here by `x`, `y`, and `z`).
// TODO: we could make some small optimization here if we have multiple values passed
- // from one function into another in the same order. This may not happen commonly enough
+ // from one function into another in the same order. This may not happen commonly eugh
// to be worth the complication though.
// NOTE: we start with 0b0 because it is important that the low bit is always 0
@@ -3022,7 +2939,8 @@
for (it in fn.valueParameters) {
val classifier = it.type.classifierOrNull
if (classifier == param.symbol) {
- val parentSlot = scope.paramsToSlots[it] ?: return null
+ val parentSlot = scope.allTrackedParams.indexOf(it)
+ if (parentSlot == -1) return null
return irAnd(
irConst(StabilityBits.UNSTABLE.bitsForSlot(0)),
maskParam.irShiftBits(parentSlot, 0)
@@ -3046,20 +2964,20 @@
override fun visitGetValue(expression: IrGetValue): IrExpression {
val declaration = expression.symbol.owner
var scope: Scope? = currentScope
- while (scope != null) {
- if (scope is Scope.FunctionScope) {
- if (scope.function.extensionReceiverParameter == declaration) {
- scope.markGetExtensionReceiver()
+ if (declaration is IrValueParameter) {
+ val fn = declaration.parent
+ while (scope != null) {
+ if (scope is Scope.FunctionScope) {
+ if (scope.function == fn) {
+ val index = scope.allTrackedParams.indexOf(declaration)
+ if (index != -1) {
+ scope.usedParams[index] = true
+ }
+ return expression
+ }
}
- if (scope.function.dispatchReceiverParameter == declaration) {
- scope.markGetDispatchReceiver()
- }
- val remapped = scope.remappedParams[declaration]
- if (remapped != null) {
- return irGet(remapped)
- }
+ scope = scope.parent
}
- scope = scope.parent
}
return expression
}
@@ -3279,8 +3197,6 @@
val function: IrFunction,
private val transformer: ComposableFunctionBodyTransformer
) : BlockScope("fun ${function.name.asString()}") {
- val remappedParams = mutableMapOf<IrValueDeclaration, IrValueDeclaration>()
- val paramsToSlots = mutableMapOf<IrValueDeclaration, Int>()
val isInlinedLambda = with(transformer) { function.isInlinedLambda() }
private var lastTemporaryIndex: Int = 0
@@ -3311,20 +3227,6 @@
var dirty: IrChangedBitMaskValue? = null
- var dispatchReceiverUsed: Boolean = false
- private set
-
- var extensionReceiverUsed: Boolean = false
- private set
-
- fun markGetDispatchReceiver() {
- dispatchReceiverUsed = true
- }
-
- fun markGetExtensionReceiver() {
- extensionReceiverUsed = true
- }
-
// Parameter information is an index from the sorted order of the parameters to the
// actual order. This is used to reorder the fields of the lambda class generated for
// restart lambdas into parameter order. If all the parameters are in sorted order
@@ -3498,6 +3400,29 @@
val isComposable = composerParameter != null
+ val allTrackedParams = listOfNotNull(
+ function.extensionReceiverParameter
+ ) + function.valueParameters.take(realValueParamCount) + listOfNotNull(
+ function.dispatchReceiverParameter
+ )
+
+ fun defaultIndexForSlotIndex(index: Int): Int {
+ return if (function.extensionReceiverParameter != null) index - 1 else index
+ }
+
+ val usedParams = BooleanArray(slotCount) { false }
+
+ init {
+ if (
+ isComposable &&
+ function.origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA
+ ) {
+ // in the case of a composable lambda, we want to make sure the dispatch
+ // receiver is always marked as "used"
+ usedParams[slotCount - 1] = true
+ }
+ }
+
fun getNameForTemporary(nameHint: String?): String {
val index = nextTemporaryIndex()
return if (nameHint != null) "tmp${index}_$nameHint" else "tmp$index"
@@ -3792,7 +3717,10 @@
}
}
+ override var used: Boolean = false
+
override fun irLowBit(): IrExpression {
+ used = true
return irAnd(
irGet(params[0]),
irConst(0b1)
@@ -3800,6 +3728,7 @@
}
override fun irIsolateBitsAtSlot(slot: Int, includeStableBit: Boolean): IrExpression {
+ used = true
// %changed and 0b11
return irAnd(
irGet(params[paramIndexForSlot(slot)]),
@@ -3814,6 +3743,7 @@
}
override fun irSlotAnd(slot: Int, bits: Int): IrExpression {
+ used = true
// %changed and 0b11
return irAnd(
irGet(params[paramIndexForSlot(slot)]),
@@ -3821,7 +3751,9 @@
)
}
- override fun irHasDifferences(): IrExpression {
+ override fun irHasDifferences(usedParams: BooleanArray): IrExpression {
+ used = true
+ require(usedParams.size == count)
if (count == 0) {
// for 0 slots (no params), we can create a shortcut expression of just checking the
// low-bit for non-zero. Since all of the higher bits will also be 0, we can just
@@ -3836,16 +3768,23 @@
val start = index * SLOTS_PER_INT
val end = min(start + SLOTS_PER_INT, count)
- // makes an int with each slot having 0b01 mask and the low bit being 0.
- // so for 3 slots, we would get 0b 01 01 01 0.
+ // makes an int with each slot having 0b101 mask and the low bit being 0.
+ // so for 3 slots, we would get 0b 101 101 101 0.
// This pattern is useful because we can and + xor it with our $changed bitmask and it
- // will only be non-zero if any of the slots were DIFFERENT or UNCERTAIN.
+ // will only be non-zero if any of the slots were DIFFERENT or UNCERTAIN or
+ // UNSTABLE.
+ // we _only_ use this pattern for the slots where the body of the function
+ // actually uses that parameter, otherwise we pass in 0b000 which will transfer
+ // none of the bits to the rhs
val lhs = (start until end).fold(0) { mask, slot ->
- mask or bitsForSlot(0b101, slot)
+ if (usedParams[slot]) mask or bitsForSlot(0b101, slot) else mask
}
+ // we _only_ use this pattern for the slots where the body of the function
+ // actually uses that parametser, otherwise we pass in 0b000 which will transfer
+ // none of the bits to the rhs
val rhs = (start until end).fold(0) { mask, slot ->
- mask or bitsForSlot(0b001, slot)
+ if (usedParams[slot]) mask or bitsForSlot(0b001, slot) else mask
}
// we use this pattern with the low bit set to 1 in the "and", and the low bit set to 0
@@ -3853,17 +3792,30 @@
// low bit. Since we use this calculation to determine if we need to run the body of the
// function, this is exactly what we want.
- // $dirty and (0b 01 ... 01 1) xor (0b 01 ... 01 0)
- irNotEqual(
- irXor(
+ // if the rhs is 0, that means that none of the parameters ended up getting used
+ // in the body of the function which means we can simplify the expression quite a
+ // bit. In this case we just care about if the low bit is non-zero
+ if (rhs == 0) {
+ irNotEqual(
irAnd(
irGet(param),
- irConst(lhs or 0b1)
+ irConst(1)
),
- irConst(rhs or 0b0)
- ),
- irConst(0) // anything non-zero means we have differences
- )
+ irConst(0)
+ )
+ } else {
+ // $dirty and (0b 101 ... 101 1) xor (0b 001 ... 001 0)
+ irNotEqual(
+ irXor(
+ irAnd(
+ irGet(param),
+ irConst(lhs or 0b1)
+ ),
+ irConst(rhs or 0b0)
+ ),
+ irConst(0) // anything non-zero means we have differences
+ )
+ }
}
return if (expressions.size == 1)
expressions.single()
@@ -3876,6 +3828,7 @@
isVar: Boolean,
exactName: Boolean
): IrChangedBitMaskVariable {
+ used = true
val temps = params.mapIndexed { index, param ->
irTemporary(
irGet(param),
@@ -3893,6 +3846,7 @@
startIndex: Int,
lowBit: Boolean
) {
+ used = true
params.forEachIndexed { index, param ->
fn.putValueArgument(
startIndex + index,
@@ -3906,6 +3860,7 @@
}
override fun irShiftBits(fromSlot: Int, toSlot: Int): IrExpression {
+ used = true
val fromSlotAdjusted = fromSlot.rem(SLOTS_PER_INT)
val toSlotAdjusted = toSlot.rem(SLOTS_PER_INT)
val bitsToShiftLeft = (toSlotAdjusted - fromSlotAdjusted) * BITS_PER_SLOT
@@ -3941,6 +3896,7 @@
}
override fun irOrSetBitsAtSlot(slot: Int, value: IrExpression): IrExpression {
+ used = true
val temp = temps[paramIndexForSlot(slot)]
return irSet(
temp,
@@ -3952,6 +3908,7 @@
}
override fun irSetSlotUncertain(slot: Int): IrExpression {
+ used = true
val temp = temps[paramIndexForSlot(slot)]
return irSet(
temp,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index fd62c1c..a598c6b 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -77,6 +77,7 @@
import org.jetbrains.kotlin.ir.symbols.IrSymbol
import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.isLocal
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
@@ -266,6 +267,8 @@
val filePath = declaration.fileEntry.name
val fileName = filePath.split('/').last()
val current = context.irFactory.buildClass {
+ startOffset = SYNTHETIC_OFFSET
+ endOffset = SYNTHETIC_OFFSET
kind = ClassKind.OBJECT
visibility = DescriptorVisibilities.INTERNAL
val shortName = PackagePartClassUtils.getFilePartShortName(fileName)
@@ -549,6 +552,8 @@
visibility = DescriptorVisibilities.INTERNAL
}.also { p ->
p.backingField = context.irFactory.buildField {
+ startOffset = SYNTHETIC_OFFSET
+ endOffset = SYNTHETIC_OFFSET
name = Name.identifier(lambdaName)
type = lambdaType
visibility = DescriptorVisibilities.INTERNAL
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
index f0d2cd9..179ceee 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/IrSourcePrinter.kt
@@ -1160,7 +1160,37 @@
}
override fun visitFunctionReference(expression: IrFunctionReference) {
- print("<<FUNCTIONREF>>")
+ val function = expression.symbol.owner
+ val dispatchReceiver = expression.dispatchReceiver
+ val extensionReceiver = expression.extensionReceiver
+ val dispatchIsSpecial = dispatchReceiver.let {
+ it is IrGetValue && it.symbol.owner.name.isSpecial
+ }
+ val extensionIsSpecial = extensionReceiver.let {
+ it is IrGetValue && it.symbol.owner.name.isSpecial
+ }
+
+ if (dispatchReceiver != null && !dispatchIsSpecial) {
+ dispatchReceiver.print()
+ print("::")
+ } else if (extensionReceiver != null && !extensionIsSpecial) {
+ extensionReceiver.print()
+ print("::")
+ }
+
+ val prop = (function as? IrSimpleFunction)?.correspondingPropertySymbol?.owner
+
+ if (prop != null) {
+ val propName = prop.name.asString()
+ print(propName)
+ if (function == prop.setter) {
+ print("::set")
+ } else if (function == prop.getter) {
+ print("::get")
+ }
+ } else {
+ print(function.name.asString())
+ }
}
override fun visitInstanceInitializerCall(expression: IrInstanceInitializerCall) {
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
index 9eae1d7..7108861 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.jvm.kt
@@ -19,6 +19,7 @@
import androidx.compose.animation.core.TweenSpec
import androidx.compose.desktop.AppWindow
import androidx.compose.desktop.DesktopMaterialTheme
+import androidx.compose.desktop.LocalAppWindow
import androidx.compose.desktop.Window
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
@@ -179,8 +180,10 @@
val amount = remember { mutableStateOf(0) }
val animation = remember { mutableStateOf(true) }
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
+ val window = LocalAppWindow.current.window
+ val info = "${window.renderApi} (${window.windowHandle})"
Text(
- text = "Привет! ä½ å¥½! Desktop Compose ${amount.value}",
+ text = "Привет! ä½ å¥½! Desktop Compose use $info: ${amount.value}",
color = Color.Black,
modifier = Modifier
.background(Color.Blue)
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
index a7c7475..4342167 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/SizeTest.kt
@@ -55,6 +55,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import org.junit.Assert.assertNotEquals
import java.util.concurrent.CountDownLatch
@@ -1807,6 +1808,7 @@
}
@Test
+ @FlakyTest(bugId = 183713100)
fun testModifiers_doNotCauseUnnecessaryRemeasure() {
var first by mutableStateOf(true)
var totalMeasures = 0
diff --git a/compose/foundation/foundation/api/1.0.0-beta04.txt b/compose/foundation/foundation/api/1.0.0-beta04.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/1.0.0-beta04.txt
@@ -425,19 +425,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
new file mode 100644
index 0000000..dd3d988
--- /dev/null
+++ b/compose/foundation/foundation/api/current.ignore
@@ -0,0 +1,10 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.TapGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.TapGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.DragGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.DragGestureFilterKt
+RemovedPackage: androidx.compose.foundation.legacygestures:
+ Removed package androidx.compose.foundation.legacygestures
+
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -425,19 +425,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
index d9e6a73..7453b13 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.0.0-beta04.txt
@@ -458,19 +458,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -684,6 +671,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index d9e6a73..7453b13 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -458,19 +458,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -684,6 +671,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt b/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
+++ b/compose/foundation/foundation/api/restricted_1.0.0-beta04.txt
@@ -425,19 +425,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
new file mode 100644
index 0000000..1db0871
--- /dev/null
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -0,0 +1,9 @@
+// Baseline format: 1.0
+RemovedClass: androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.PressIndicatorGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.TapGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.TapGestureFilterKt
+RemovedClass: androidx.compose.foundation.legacygestures.DragGestureFilterKt:
+ Removed class androidx.compose.foundation.legacygestures.DragGestureFilterKt
+RemovedPackage: androidx.compose.foundation.legacygestures:
+ Removed package androidx.compose.foundation.legacygestures
\ No newline at end of file
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index bff54f1..f85c7ad 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -425,19 +425,6 @@
}
-package androidx.compose.foundation.legacygestures {
-
- public final class DragGestureFilterKt {
- }
-
- public final class PressIndicatorGestureFilterKt {
- }
-
- public final class TapGestureFilterKt {
- }
-
-}
-
package androidx.compose.foundation.selection {
public final class SelectableGroupKt {
@@ -648,6 +635,9 @@
property public final androidx.compose.foundation.text.KeyboardOptions Default;
}
+ public final class LongPressTextDragObserverKt {
+ }
+
public final class MaxLinesHeightModifierKt {
}
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
similarity index 98%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
index 02b991d..6ddf510 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextBasicBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextBasicBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
index 3e758b9..14f1941 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextBenchmark.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.test.filters.SmallTest
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
@@ -77,7 +76,6 @@
* given input. This is the time taken to call the [BasicTextField] composable function.
*/
@Test
- @Ignore("b/170960653")
fun first_compose() {
benchmarkRule.benchmarkFirstComposeFast(caseFactory)
}
@@ -88,7 +86,6 @@
* [BasicTextField] composable.
*/
@Test
- @Ignore("b/170960653")
fun first_measure() {
benchmarkRule.benchmarkFirstMeasureFast(caseFactory)
}
@@ -98,7 +95,6 @@
* given input.
*/
@Test
- @Ignore("b/170960653")
fun first_layout() {
benchmarkRule.benchmarkFirstLayoutFast(caseFactory)
}
@@ -108,7 +104,6 @@
* input.
*/
@Test
- @Ignore("b/170960653")
fun first_draw() {
benchmarkRule.benchmarkFirstDrawFast(caseFactory)
}
@@ -118,7 +113,6 @@
* constrains changed. This is mainly the time used to re-measure and re-layout the composable.
*/
@Test
- @Ignore("b/170960653")
fun layout() {
benchmarkRule.benchmarkLayoutPerf(caseFactory)
}
@@ -127,7 +121,6 @@
* Measure the time taken by redrawing the [BasicTextField] composable.
*/
@Test
- @Ignore("b/170960653")
fun draw() {
benchmarkRule.benchmarkDrawPerf(caseFactory)
}
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
index 960dc16..56ed53c 100644
--- a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextFieldToggleTextTestCase.kt
@@ -21,7 +21,9 @@
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.mutableStateOf
@@ -72,7 +74,7 @@
@Composable
override fun ContentWrappers(content: @Composable () -> Unit) {
Column(
- modifier = Modifier.width(width)
+ modifier = Modifier.width(width).verticalScroll(rememberScrollState())
) {
CompositionLocalProvider(LocalTextInputService provides textInputService) {
content()
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
similarity index 90%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
index 6e40cd6..e0662cc 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextInColumnTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextInColumnTestCase.kt
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -53,6 +55,7 @@
override fun ContentWrappers(content: @Composable () -> Unit) {
Column(
modifier = Modifier.wrapContentSize(Alignment.Center).width(width)
+ .verticalScroll(rememberScrollState())
) {
content()
}
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
similarity index 98%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
index ce699dd..5995d6d 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextMultiStyleBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextMultiStyleBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.benchmarkDrawPerf
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
similarity index 97%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
index 01310a5..918e3a1 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextBenchmark.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextBenchmark.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
import androidx.compose.testutils.benchmark.toggleStateBenchmarkDraw
diff --git a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
similarity index 90%
rename from compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt
rename to compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
index a8b99cb..c4ea2ab 100644
--- a/compose/material/material/benchmark/src/androidTest/java/androidx/compose/material/benchmark/TextToggleTextTestCase.kt
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/TextToggleTextTestCase.kt
@@ -14,11 +14,13 @@
* limitations under the License.
*/
-package androidx.compose.material.benchmark
+package androidx.compose.foundation.benchmark.text
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -47,6 +49,7 @@
override fun Content() {
Column(
modifier = Modifier.wrapContentSize(Alignment.Center).width(width)
+ .verticalScroll(rememberScrollState())
) {
for (text in texts) {
Text(text = text.value, color = Color.Black, fontSize = fontSize)
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt
new file mode 100644
index 0000000..599098f
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerBenchmark.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.benchmark.text.selection
+
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkDrawPerf
+import androidx.compose.testutils.benchmark.benchmarkFirstCompose
+import androidx.compose.testutils.benchmark.benchmarkFirstDraw
+import androidx.compose.testutils.benchmark.benchmarkFirstLayout
+import androidx.compose.testutils.benchmark.benchmarkFirstMeasure
+import androidx.compose.testutils.benchmark.benchmarkLayoutPerf
+import androidx.test.filters.SmallTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@SmallTest
+@RunWith(Parameterized::class)
+class SelectionContainerBenchmark(private val childrenCount: Int) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(
+ name = "childrenCount={0}"
+ )
+ fun initParameters() = arrayOf(1, 10, 20)
+ }
+
+ @get:Rule
+ val benchmarkRule = ComposeBenchmarkRule()
+ private val caseFactory = { SelectionContainerTestCase(childrenCount) }
+
+ @Test
+ fun first_compose() {
+ benchmarkRule.benchmarkFirstCompose(caseFactory)
+ }
+
+ @Test
+ fun first_measure() {
+ benchmarkRule.benchmarkFirstMeasure(caseFactory)
+ }
+
+ @Test
+ fun first_layout() {
+ benchmarkRule.benchmarkFirstLayout(caseFactory)
+ }
+
+ @Test
+ fun first_draw() {
+ benchmarkRule.benchmarkFirstDraw(caseFactory)
+ }
+
+ @Test
+ fun layout() {
+ benchmarkRule.benchmarkLayoutPerf(caseFactory)
+ }
+
+ @Test
+ fun draw() {
+ benchmarkRule.benchmarkDrawPerf(caseFactory)
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
new file mode 100644
index 0000000..e94b226
--- /dev/null
+++ b/compose/foundation/foundation/benchmark/src/androidTest/java/androidx/compose/foundation/benchmark/text/selection/SelectionContainerTestCase.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.benchmark.text.selection
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.text.selection.SelectionContainer
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.sp
+
+class SelectionContainerTestCase(private val childrenCount: Int) : ComposeTestCase {
+ @Composable
+ override fun Content() {
+ SelectionContainer {
+ Column {
+ repeat(childrenCount) {
+ Text(
+ text = "Hello World Hello World Hello W",
+ style = TextStyle(fontSize = 20.sp)
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
index 56f1900..816b805 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/CapitalizationAutoCorrectDemo.kt
@@ -97,7 +97,7 @@
modifier = demoTextFieldModifiers.defaultMinSize(100.dp),
value = state,
keyboardOptions = data.keyboardOptions,
- keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+ keyboardActions = KeyboardActions { keyboardController?.hide() },
onValueChange = { state = it },
textStyle = TextStyle(fontSize = fontSize8),
cursorBrush = SolidColor(Color.Red)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
index e908c57..edc57c1 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
@@ -92,7 +92,7 @@
keyboardType = keyboardType,
imeAction = imeAction
),
- keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+ keyboardActions = KeyboardActions { keyboardController?.hide() },
onValueChange = { state.value = it },
textStyle = TextStyle(fontSize = fontSize8),
)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
index 24cd4c9..0113667 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/KeyboardSingleLineDemo.kt
@@ -125,7 +125,7 @@
modifier = demoTextFieldModifiers.defaultMinSize(100.dp),
value = state.value,
keyboardOptions = data.keyboardOptions,
- keyboardActions = KeyboardActions { keyboardController?.hideSoftwareKeyboard() },
+ keyboardActions = KeyboardActions { keyboardController?.hide() },
singleLine = data.singleLine,
onValueChange = { state.value = it },
textStyle = TextStyle(fontSize = fontSize8),
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index b53e3ce..f3e0470 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -41,6 +41,7 @@
import androidx.compose.ui.test.click
import androidx.compose.ui.test.doubleClick
import androidx.compose.ui.test.down
+import androidx.compose.ui.test.cancel
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithTag
@@ -467,6 +468,57 @@
}
@Test
+ fun clickableTest_interactionSource_cancelledGesture() {
+ val interactionSource = MutableInteractionSource()
+
+ var scope: CoroutineScope? = null
+
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ Box {
+ BasicText(
+ "ClickableText",
+ modifier = Modifier
+ .testTag("myClickable")
+ .combinedClickable(
+ interactionSource = interactionSource,
+ indication = null
+ ) {}
+ )
+ }
+ }
+
+ val interactions = mutableListOf<Interaction>()
+
+ scope!!.launch {
+ interactionSource.interactions.collect { interactions.add(it) }
+ }
+
+ rule.runOnIdle {
+ assertThat(interactions).isEmpty()
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .performGesture { down(center) }
+
+ rule.runOnIdle {
+ assertThat(interactions).hasSize(1)
+ assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
+ }
+
+ rule.onNodeWithTag("myClickable")
+ .performGesture { cancel() }
+
+ rule.runOnIdle {
+ assertThat(interactions).hasSize(2)
+ assertThat(interactions.first()).isInstanceOf(PressInteraction.Press::class.java)
+ assertThat(interactions[1]).isInstanceOf(PressInteraction.Cancel::class.java)
+ assertThat((interactions[1] as PressInteraction.Cancel).press)
+ .isEqualTo(interactions[0])
+ }
+ }
+
+ @Test
fun clickableTest_interactionSource_resetWhenDisposed() {
val interactionSource = MutableInteractionSource()
var emitClickableText by mutableStateOf(true)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
index 9f259df..b24e18c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListLayoutInfoTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -126,14 +127,14 @@
}
}
+ @Composable
+ fun ObservingFun(state: LazyListState, currentInfo: StableRef<LazyListLayoutInfo?>) {
+ currentInfo.value = state.layoutInfo
+ }
@Test
fun visibleItemsAreObservableWhenWeScroll() {
lateinit var state: LazyListState
- var currentInfo: LazyListLayoutInfo? = null
- @Composable
- fun observingFun() {
- currentInfo = state.layoutInfo
- }
+ val currentInfo = StableRef<LazyListLayoutInfo?>(null)
rule.setContent {
LazyColumn(
state = rememberLazyListState().also { state = it },
@@ -144,20 +145,20 @@
Box(Modifier.requiredSize(itemSizeDp))
}
}
- observingFun()
+ ObservingFun(state, currentInfo)
}
rule.runOnIdle {
// empty it here and scrolling should invoke observingFun again
- currentInfo = null
+ currentInfo.value = null
runBlocking {
state.scrollToItem(1, 0)
}
}
rule.runOnIdle {
- assertThat(currentInfo).isNotNull()
- currentInfo!!.assertVisibleItems(count = 4, startIndex = 1)
+ assertThat(currentInfo.value).isNotNull()
+ currentInfo.value!!.assertVisibleItems(count = 4, startIndex = 1)
}
}
@@ -291,3 +292,6 @@
}
}
}
+
+@Stable
+class StableRef<T>(var value: T)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
new file mode 100644
index 0000000..f282c7d
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextDelegateWidthWithLetterSpacingTest.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.text.selection.BASIC_MEASURE_FONT
+import androidx.compose.foundation.text.selection.TestFontResourceLoader
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(InternalFoundationTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TextDelegateWidthWithLetterSpacingTest {
+ private val fontFamily = BASIC_MEASURE_FONT.toFontFamily()
+
+ /**
+ * values are exact values for the repro case (on Pixel4, Android 11)
+ */
+ private val density = Density(3.051f, 1.15f)
+ private val letterSpacing = 0.4.sp
+ private val lineHeight = 16.sp
+ private val fontSize = 12.sp
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val resourceLoader = TestFontResourceLoader(context)
+
+ @Test
+ fun letterSpacing_and_lineHeight() {
+ assertLineCount(
+ TextStyle(letterSpacing = letterSpacing, lineHeight = lineHeight)
+ )
+ }
+
+ @Test
+ fun only_letterSpacing() {
+ assertLineCount(TextStyle(letterSpacing = letterSpacing))
+ }
+
+ @Test
+ fun only_lineHeight() {
+ assertLineCount(TextStyle(lineHeight = lineHeight))
+ }
+
+ @Test
+ fun no_lineHeight_or_letterSpacing() {
+ assertLineCount(TextStyle())
+ }
+
+ private fun assertLineCount(style: TextStyle) {
+ val textDelegate = TextDelegate(
+ text = AnnotatedString(text = "This is a callout message"),
+ style = style.copy(
+ fontFamily = fontFamily,
+ fontSize = fontSize
+ ),
+ softWrap = true,
+ overflow = TextOverflow.Clip,
+ density = density,
+ resourceLoader = resourceLoader
+ )
+ val layoutResult = textDelegate.layout(Constraints(), LayoutDirection.Ltr)
+ assertThat(layoutResult.lineCount).isEqualTo(1)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
new file mode 100644
index 0000000..1e643bc
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextPreparedSelectionTest.kt
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.text.selection.BaseTextPreparedSelection
+import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
+import androidx.compose.foundation.text.selection.TextPreparedSelection
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.test.R
+import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class TextPreparedSelectionTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun textSelection_leftRightMovements() {
+ selectionTest("abc") {
+ it.moveCursorRight()
+ expectedSelection(cursorAt('b'))
+ it.moveCursorRight()
+ expectedSelection(cursorAt('c'))
+ it.moveCursorRight()
+ expectedSelection(cursorAfter('c'))
+ it.moveCursorRight()
+ expectedSelection(cursorAfter('c'))
+ it.moveCursorLeft()
+ expectedSelection(cursorAt('c'))
+ }
+ }
+
+ @Test
+ fun textSelection_leftRightMovements_rtl() {
+ selectionTest("\u0671\u0679\u0683", rtl = true) {
+ expectedSelection(cursorAt('\u0671'))
+ it.moveCursorLeft()
+ expectedSelection(cursorAt('\u0679'))
+ it.moveCursorLeft()
+ expectedSelection(cursorAt('\u0683'))
+ it.moveCursorRight()
+ expectedSelection(cursorAt('\u0679'))
+ }
+ }
+
+ @Test
+ fun textSelection_leftRightMovements_bidi() {
+ selectionTest("ab \u0671\u0679\u0683 cd") {
+ it.moveCursorRight()
+ expectedSelection(TextRange(1))
+ it.moveCursorRight()
+ expectedSelection(TextRange(2))
+ it.moveCursorRight()
+ expectedSelection(TextRange(3))
+ it.moveCursorRight()
+ expectedSelection(TextRange(4))
+ }
+ }
+
+ @Test
+ fun textSelection_byWordMovements() {
+ selectionTest("abc def\n\ngi") {
+ it.moveCursorRightByWord()
+ expectedSelection(cursorAfter('c'))
+ it.moveCursorRightByWord()
+ expectedSelection(cursorAfter('f'))
+ it.moveCursorLeftByWord()
+ expectedSelection(cursorAt('d'))
+ it.moveCursorRightByWord()
+ expectedSelection(cursorAfter('f'))
+ it.moveCursorRightByWord()
+ expectedSelection(cursorAfter('i'))
+ }
+ }
+
+ @Test
+ fun textSelection_lineMovements() {
+ selectionTest("ab\ncde\n\ngi", initSelection = TextRange(1)) {
+ it.moveCursorDownByLine()
+ expectedSelection(cursorAt('d'))
+ it.moveCursorDownByLine()
+ // at empty line
+ expectedSelection(TextRange(7))
+ it.moveCursorDownByLine()
+ // cursor should be at "cached" x-position
+ expectedSelection(cursorAt('i'))
+ it.moveCursorDownByLine()
+ expectedSelection(cursorAfter('i'))
+ it.moveCursorUpByLine()
+ it.moveCursorUpByLine()
+ // and again, it should be recovered at "cached" x-position
+ expectedSelection(cursorAt('d'))
+ it.moveCursorLeft()
+ it.moveCursorUpByLine()
+ // after horizontal move, "cached" x-position should be reset
+ expectedSelection(cursorAt('a'))
+ }
+ }
+
+ private inner class SelectionScope<T : BaseTextPreparedSelection<T>>(
+ val prepared: BaseTextPreparedSelection<T>
+ ) {
+ fun expectedText(text: String) {
+ rule.runOnIdle {
+ Truth.assertThat(prepared.text).isEqualTo(text)
+ }
+ }
+
+ fun expectedSelection(selection: TextRange) {
+ rule.runOnIdle {
+ Truth.assertThat(prepared.selection).isEqualTo(selection)
+ }
+ }
+
+ fun cursorAt(char: Char) =
+ TextRange(prepared.text.indexOf(char))
+
+ fun cursorAfter(char: Char) =
+ TextRange(prepared.text.indexOf(char) + 1)
+ }
+
+ private fun selectionTest(
+ initText: String = "",
+ initSelection: TextRange = TextRange(0),
+ rtl: Boolean = false,
+ test: SelectionScope<TextPreparedSelection>.(TextPreparedSelection) -> Unit
+ ) {
+ var textLayout: TextLayoutResult? = null
+ val direction = if (rtl) {
+ LayoutDirection.Rtl
+ } else {
+ LayoutDirection.Ltr
+ }
+ rule.setContent {
+ CompositionLocalProvider(LocalLayoutDirection provides direction) {
+ BasicText(
+ text = initText,
+ style = TextStyle(
+ fontFamily = Font(
+ R.font.sample_font,
+ FontWeight.Normal,
+ FontStyle.Normal
+ ).toFontFamily()
+ ),
+ onTextLayout = { textLayout = it }
+ )
+ }
+ }
+
+ val prepared = TextPreparedSelection(
+ originalText = AnnotatedString(initText),
+ originalSelection = initSelection,
+ layoutResult = textLayout!!
+ )
+
+ test(SelectionScope(prepared), prepared)
+ }
+
+ private fun textFieldSelectionTest(
+ initText: String = "",
+ initSelection: TextRange = TextRange(0),
+ test: SelectionScope<TextFieldPreparedSelection>.(TextFieldPreparedSelection) -> Unit
+ ) {
+ var textLayout: TextLayoutResult? = null
+ rule.setContent {
+ BasicText(
+ text = initText,
+ style = TextStyle(
+ fontFamily = Font(
+ R.font.sample_font,
+ FontWeight.Normal,
+ FontStyle.Normal
+ ).toFontFamily()
+ ),
+ onTextLayout = { textLayout = it }
+ )
+ }
+
+ val prepared = TextFieldPreparedSelection(
+ currentValue = TextFieldValue(initText, initSelection),
+ layoutResultProxy = TextLayoutResultProxy(textLayout!!)
+ )
+
+ test(SelectionScope(prepared), prepared)
+ }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
index db16b94..a55c0cf 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HardwareKeyboardTest.kt
@@ -21,6 +21,7 @@
import android.view.KeyEvent.META_SHIFT_ON
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
@@ -36,8 +37,16 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.performKeyPress
import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.test.R
+import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth
@@ -89,6 +98,17 @@
}
@Test
+ fun textField_linesNavigation_cache() {
+ keysSequenceTest(initText = "hello\n\nworld") {
+ Key.DirectionRight.downAndUp()
+ Key.DirectionDown.downAndUp()
+ Key.DirectionDown.downAndUp()
+ Key.Zero.downAndUp()
+ expectedText("hello\n\nw0orld")
+ }
+ }
+
+ @Test
fun textField_newLine() {
keysSequenceTest(initText = "hello") {
Key.Enter.downAndUp()
@@ -228,6 +248,17 @@
}
}
+ @Test
+ fun textField_pageNavigation() {
+ keysSequenceTest(
+ initText = "1\n2\n3\n4\n5",
+ modifier = Modifier.requiredSize(30.dp)
+ ) {
+ Key.PageDown.downAndUp()
+ expectedSelection(TextRange(4))
+ }
+ }
+
private inner class SequenceScope(
val state: MutableState<TextFieldValue>,
val nodeGetter: () -> SemanticsNodeInteraction
@@ -260,6 +291,7 @@
private fun keysSequenceTest(
initText: String = "",
+ modifier: Modifier = Modifier.fillMaxSize(),
sequence: SequenceScope.() -> Unit
) {
val inputService = TextInputService(mock())
@@ -272,7 +304,15 @@
) {
BasicTextField(
value = state.value,
- modifier = Modifier.fillMaxSize().focusRequester(focusFequester),
+ textStyle = TextStyle(
+ fontFamily = Font(
+ R.font.sample_font,
+ FontWeight.Normal,
+ FontStyle.Normal
+ ).toFontFamily(),
+ fontSize = 10.sp
+ ),
+ modifier = modifier.focusRequester(focusFequester),
onValueChange = {
state.value = it
}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
index e8a0677..9dddcdbe 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/AndroidSelectionHandles.android.kt
@@ -18,8 +18,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.AbsoluteAlignment
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
@@ -27,12 +25,7 @@
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.text.style.ResolvedTextDirection
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
@@ -41,7 +34,6 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
-import kotlin.math.max
import kotlin.math.roundToInt
@Composable
@@ -175,59 +167,33 @@
content: @Composable () -> Unit
) {
val offset = (if (isStartHandle) startHandlePosition else endHandlePosition) ?: return
+ val left = isLeft(
+ isStartHandle = isStartHandle,
+ directions = directions,
+ handlesCrossed = handlesCrossed
+ )
- SimpleLayout(AllowZeroSize) {
- val left = isLeft(
- isStartHandle = isStartHandle,
- directions = directions,
- handlesCrossed = handlesCrossed
- )
- val alignment = if (left) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft
+ val intOffset = IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
- val intOffset = IntOffset(offset.x.roundToInt(), offset.y.roundToInt())
-
- val popupPositioner = remember(alignment, intOffset) {
- SelectionHandlePositionProvider(alignment, intOffset)
- }
-
- Popup(
- popupPositionProvider = popupPositioner,
- content = content
- )
+ val popupPositioner = remember(left, intOffset) {
+ SelectionHandlePositionProvider(left, intOffset)
}
+
+ Popup(
+ popupPositionProvider = popupPositioner,
+ content = content
+ )
}
/**
- * This modifier allows the content to measure at its desired size without regard for the incoming
- * measurement [minimum width][Constraints.minWidth] or [minimum height][Constraints.minHeight]
- * constraints.
- *
- * The same as "wrapContentSize" in foundation-layout, which we cannot use in this module.
- */
-private object AllowZeroSize : LayoutModifier {
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val placeable = measurable.measure(constraints.copy(minWidth = 0, minHeight = 0))
- return layout(
- max(constraints.minWidth, placeable.width),
- max(constraints.minHeight, placeable.height)
- ) {
- placeable.place(0, 0)
- }
- }
-}
-
-/**
- * This is a copy of "AlignmentOffsetPositionProvider" class in Popup, with some
- * change at "resolvedOffset" value.
- *
- * This is for [SelectionHandlePopup] only.
+ * This [PopupPositionProvider] for [SelectionHandlePopup]. It will position the selection handle
+ * to the [offset] in its anchor layout. For left selection handle, the right top corner will be
+ * positioned to [offset]. For right selection handle, the left top corner will be positioned to
+ * [offset].
*/
/*@VisibleForTesting*/
internal class SelectionHandlePositionProvider(
- val alignment: Alignment,
+ val isLeft: Boolean,
val offset: IntOffset
) : PopupPositionProvider {
override fun calculatePosition(
@@ -236,36 +202,17 @@
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
- // TODO: Decide which is the best way to round to result without reimplementing Alignment.align
- var popupPosition = IntOffset(0, 0)
-
- // Get the aligned point inside the parent
- val parentAlignmentPoint = alignment.align(
- IntSize.Zero,
- IntSize(anchorBounds.width, anchorBounds.height),
- layoutDirection
- )
- // Get the aligned point inside the child
- val relativePopupPos = alignment.align(
- IntSize.Zero,
- IntSize(popupContentSize.width, popupContentSize.height),
- layoutDirection
- )
-
- // Add the position of the parent
- popupPosition += IntOffset(anchorBounds.left, anchorBounds.top)
-
- // Add the distance between the parent's top left corner and the alignment point
- popupPosition += parentAlignmentPoint
-
- // Subtract the distance between the children's top left corner and the alignment point
- popupPosition -= IntOffset(relativePopupPos.x, relativePopupPos.y)
-
- // Add the user offset
- val resolvedOffset = IntOffset(offset.x, offset.y)
- popupPosition += resolvedOffset
-
- return popupPosition
+ return if (isLeft) {
+ IntOffset(
+ x = anchorBounds.left + offset.x - popupContentSize.width,
+ y = anchorBounds.top + offset.y
+ )
+ } else {
+ IntOffset(
+ x = anchorBounds.left + offset.x,
+ y = anchorBounds.top + offset.y
+ )
+ }
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 95c6335..04c9bddb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -16,19 +16,20 @@
package androidx.compose.foundation
+import androidx.compose.foundation.gestures.PressGestureScope
+import androidx.compose.foundation.gestures.detectTapAndPress
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
@@ -37,7 +38,6 @@
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
-import kotlinx.coroutines.launch
/**
* Configure component to receive clicks via input or accessibility "click" event.
@@ -109,7 +109,6 @@
* to describe the element or do customizations
* @param onClick will be called when user clicks on the element
*/
-@Suppress("DEPRECATION")
fun Modifier.clickable(
interactionSource: MutableInteractionSource,
indication: Indication?,
@@ -119,59 +118,24 @@
onClick: () -> Unit
) = composed(
factory = {
- val scope = rememberCoroutineScope()
+ val onClickState = rememberUpdatedState(onClick)
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
- val interactionUpdate =
- if (enabled) {
- Modifier.pressIndicatorGestureFilter(
- onStart = {
- scope.launch {
- // Remove any old interactions if we didn't fire stop / cancel properly
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- val interaction = PressInteraction.Press(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = interaction
- }
+ val gesture = if (enabled) {
+ PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+ Modifier.pointerInput(interactionSource) {
+ detectTapAndPress(
+ onPress = { offset ->
+ handlePressInteraction(offset, interactionSource, pressedInteraction)
},
- onStop = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Release(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- }
- },
- onCancel = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Cancel(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- }
- }
+ onTap = { onClickState.value.invoke() }
)
- } else {
- Modifier
}
- val tap = if (enabled) tapGestureFilter(onTap = { onClick() }) else Modifier
- DisposableEffect(interactionSource) {
- onDispose {
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSource.tryEmit(interaction)
- pressedInteraction.value = null
- }
- }
+ } else {
+ Modifier
}
Modifier
.genericClickableWithoutGesture(
- gestureModifiers = Modifier.then(interactionUpdate).then(tap),
+ gestureModifiers = gesture,
interactionSource = interactionSource,
indication = indication,
enabled = enabled,
@@ -292,12 +256,11 @@
onClick: () -> Unit
) = composed(
factory = {
- val scope = rememberCoroutineScope()
val onClickState = rememberUpdatedState(onClick)
- val interactionSourceState = rememberUpdatedState(interactionSource)
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
val gesture = if (enabled) {
- Modifier.pointerInput(onDoubleClick, onLongClick) {
+ PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+ Modifier.pointerInput(onDoubleClick, onLongClick, interactionSource) {
detectTapGestures(
onDoubleTap = if (onDoubleClick != null) {
{ onDoubleClick() }
@@ -309,26 +272,8 @@
} else {
null
},
- onPress = {
- scope.launch {
- // Remove any old interactions if we didn't fire stop / cancel properly
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSourceState.value.emit(interaction)
- pressedInteraction.value = null
- }
- val interaction = PressInteraction.Press(it)
- interactionSourceState.value.emit(interaction)
- pressedInteraction.value = interaction
- }
- tryAwaitRelease()
- scope.launch {
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Release(oldValue)
- interactionSourceState.value.emit(interaction)
- pressedInteraction.value = null
- }
- }
+ onPress = { offset ->
+ handlePressInteraction(offset, interactionSource, pressedInteraction)
},
onTap = { onClickState.value.invoke() }
)
@@ -336,17 +281,6 @@
} else {
Modifier
}
- DisposableEffect(interactionSource) {
- onDispose {
- scope.launch {
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSourceState.value.emit(interaction)
- pressedInteraction.value = null
- }
- }
- }
- }
Modifier
.genericClickableWithoutGesture(
gestureModifiers = gesture,
@@ -375,6 +309,41 @@
)
@Composable
+internal fun PressedInteractionSourceDisposableEffect(
+ interactionSource: MutableInteractionSource,
+ pressedInteraction: MutableState<PressInteraction.Press?>
+) {
+ DisposableEffect(interactionSource) {
+ onDispose {
+ pressedInteraction.value?.let { oldValue ->
+ val interaction = PressInteraction.Cancel(oldValue)
+ interactionSource.tryEmit(interaction)
+ pressedInteraction.value = null
+ }
+ }
+ }
+}
+
+internal suspend fun PressGestureScope.handlePressInteraction(
+ pressPoint: Offset,
+ interactionSource: MutableInteractionSource,
+ pressedInteraction: MutableState<PressInteraction.Press?>
+) {
+ val pressInteraction = PressInteraction.Press(pressPoint)
+ interactionSource.emit(pressInteraction)
+ pressedInteraction.value = pressInteraction
+ val success = tryAwaitRelease()
+ val endInteraction =
+ if (success) {
+ PressInteraction.Release(pressInteraction)
+ } else {
+ PressInteraction.Cancel(pressInteraction)
+ }
+ interactionSource.emit(endInteraction)
+ pressedInteraction.value = null
+}
+
+@Composable
@Suppress("ComposableModifierFactory")
internal fun Modifier.genericClickableWithoutGesture(
gestureModifiers: Modifier,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index f12d7de..91c9ce7 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -23,8 +23,10 @@
import androidx.compose.ui.input.pointer.PointerId
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.isOutOfBounds
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
@@ -33,6 +35,7 @@
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastFirstOrNull
+import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
@@ -247,10 +250,16 @@
onDragStart.invoke(drag.position)
awaitPointerEventScope {
- if (!drag(drag.id) { onDrag(it, it.positionChange()) }) {
- onDragCancel()
- } else {
+ if (drag(drag.id) { onDrag(it, it.positionChange()) }) {
+ // consume up if we quit drag gracefully with the up
+ currentEvent.changes.fastForEach {
+ if (it.changedToUp()) {
+ it.consumeDownChange()
+ }
+ }
onDragEnd()
+ } else {
+ onDragCancel()
}
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
index d05b451c..c2e33b6d6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ForEachGesture.kt
@@ -19,15 +19,14 @@
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.util.fastAny
-import kotlinx.coroutines.InternalCoroutinesApi
-import kotlinx.coroutines.NonCancellable.isActive
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.isActive
import kotlin.coroutines.cancellation.CancellationException
/**
* A gesture was canceled and cannot continue, likely because another gesture has taken
* over the pointer input stream.
*/
-@OptIn(ExperimentalStdlibApi::class)
class GestureCancellationException(message: String? = null) : CancellationException(message)
/**
@@ -35,9 +34,9 @@
* it will wait until all pointers are raised before another gesture is detected, or it
* exits if [isActive] is `false`.
*/
-@OptIn(InternalCoroutinesApi::class, ExperimentalStdlibApi::class)
suspend fun PointerInputScope.forEachGesture(block: suspend PointerInputScope.() -> Unit) {
- while (isActive) {
+ val currentContext = currentCoroutineContext()
+ while (currentContext.isActive) {
try {
block()
@@ -45,7 +44,7 @@
awaitAllPointersUp()
} catch (e: CancellationException) {
// The gesture was canceled. Wait for all fingers to be "up" before looping again.
- if (isActive) {
+ if (currentContext.isActive) {
awaitAllPointersUp()
throw e
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index 6235252..7739b9b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -16,26 +16,38 @@
package androidx.compose.foundation.gestures
+import androidx.compose.foundation.gestures.TapGestureEvent.AllUp
+import androidx.compose.foundation.gestures.TapGestureEvent.Cancel
+import androidx.compose.foundation.gestures.TapGestureEvent.Down
+import androidx.compose.foundation.gestures.TapGestureEvent.Up
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEvent
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
import androidx.compose.ui.input.pointer.changedToUp
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.isOutOfBounds
+import androidx.compose.ui.input.pointer.positionChangeConsumed
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.unit.Density
import androidx.compose.ui.util.fastAll
import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastForEach
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withTimeout
@@ -87,59 +99,57 @@
onLongPress: ((Offset) -> Unit)? = null,
onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
onTap: ((Offset) -> Unit)? = null
-) {
- val pressScope = PressGestureScopeImpl(this)
- forEachGesture {
- coroutineScope {
+) = coroutineScope {
+ // special signal to indicate to the sending side that it needs to consume(!) all the events
+ // until all the pointers will be up
+ val consumeAllUntilUpSignal = mutableStateOf(false)
+ // special signal to indicate to the sending side that it shouldn't intercept and consume
+ // cancel/up events as we're only require down events
+ val consumeOnlyDownsSignal = mutableStateOf(false)
+ val channel = Channel<TapGestureEvent>(capacity = Channel.UNLIMITED)
+ val pressScope = PressGestureScopeImpl(this@detectTapGestures)
+
+ launch {
+ while (isActive) {
pressScope.reset()
- val down = awaitPointerEventScope {
- awaitFirstDown().also {
- it.consumeDownChange()
- }
- }
- if (onPress !== NoPressGesture) {
- launch { pressScope.onPress(down.position) }
- }
-
- val longPressTimeout =
- if (onLongPress == null) {
- Int.MAX_VALUE.toLong()
- } else {
- viewConfiguration.longPressTimeoutMillis
- }
-
- var up: PointerInputChange? = null
+ consumeAllUntilUpSignal.value = false
+ val down = awaitChannelDown(consumeOnlyDownsSignal, channel)
+ if (onPress !== NoPressGesture) launch { pressScope.onPress(down.position) }
+ val longPressTimeout = onLongPress?.let { viewConfiguration.longPressTimeoutMillis }
+ var upOrCancel: TapGestureEvent? = null
try {
// wait for first tap up or long press
- up = withTimeout(longPressTimeout) {
- awaitPointerEventScope {
- waitForUpOrCancellation()?.also { it.consumeDownChange() }
- }
+ upOrCancel = withNullableTimeout(this, longPressTimeout) {
+ awaitChannelUpOrCancel(channel)
}
- if (up == null) {
+ if (upOrCancel is Cancel) {
pressScope.cancel() // tap-up was canceled
} else {
pressScope.release()
}
} catch (_: TimeoutCancellationException) {
onLongPress?.invoke(down.position)
- consumeAllEventsUntilUp()
+ awaitChannelAllUp(consumeAllUntilUpSignal, channel)
pressScope.release()
}
- if (up != null) {
+ if (upOrCancel != null && upOrCancel is Up) {
// tap was successful.
if (onDoubleTap == null) {
- onTap?.invoke(up.position) // no need to check for double-tap.
+ onTap?.invoke(upOrCancel.position) // no need to check for double-tap.
} else {
// check for second tap
- val secondDown = detectSecondTapDown(up.uptimeMillis)
+ val secondDown = awaitChannelSecondDown(
+ channel,
+ consumeOnlyDownsSignal,
+ viewConfiguration,
+ upOrCancel
+ )
if (secondDown == null) {
- onTap?.invoke(up.position) // no valid second tap started
+ onTap?.invoke(upOrCancel.position) // no valid second tap started
} else {
// Second tap down detected
- secondDown.consumeDownChange()
pressScope.reset()
if (onPress !== NoPressGesture) {
launch { pressScope.onPress(secondDown.position) }
@@ -147,27 +157,24 @@
try {
// Might have a long second press as the second tap
- withTimeout(longPressTimeout) {
- awaitPointerEventScope {
- val secondUp = waitForUpOrCancellation()
- if (secondUp == null) {
- pressScope.cancel()
- onTap?.invoke(up.position)
- } else {
- secondUp.consumeDownChange()
- pressScope.release()
- onDoubleTap(secondUp.position)
- }
+ withNullableTimeout(this, longPressTimeout) {
+ val secondUp = awaitChannelUpOrCancel(channel)
+ if (secondUp is Up) {
+ pressScope.release()
+ onDoubleTap(secondUp.position)
+ } else {
+ pressScope.cancel()
+ onTap?.invoke(upOrCancel.position)
}
}
} catch (e: TimeoutCancellationException) {
// The first tap was valid, but the second tap is a long press.
// notify for the first tap
- onTap?.invoke(up.position)
+ onTap?.invoke(upOrCancel.position)
// notify for the long press
onLongPress?.invoke(secondDown.position)
- consumeAllEventsUntilUp()
+ awaitChannelAllUp(consumeAllUntilUpSignal, channel)
pressScope.release()
}
}
@@ -175,6 +182,202 @@
}
}
}
+ forEachGesture {
+ awaitPointerEventScope {
+ translatePointerEventsToChannel(
+ this@coroutineScope,
+ channel,
+ consumeOnlyDownsSignal,
+ consumeAllUntilUpSignal
+ )
+ }
+ }
+}
+
+private suspend fun <T> withNullableTimeout(
+ scope: CoroutineScope,
+ timeout: Long?,
+ block: suspend CoroutineScope.() -> T
+): T {
+ return if (timeout != null) {
+ withTimeout(timeout, block)
+ } else {
+ with(scope) {
+ block()
+ }
+ }
+}
+
+/**
+ * Await down from the channel and return it when it happens
+ */
+private suspend fun awaitChannelDown(
+ onlyDownsSignal: MutableState<Boolean>,
+ channel: ReceiveChannel<TapGestureEvent>
+): Down {
+ onlyDownsSignal.value = true
+ var event = channel.receive()
+ while (event !is Down) {
+ event = channel.receive()
+ }
+ onlyDownsSignal.value = false
+ return event
+}
+
+/**
+ * Reads input for second tap down event from the [channel]. If the second tap is within
+ * [ViewConfiguration.doubleTapMinTimeMillis] of [firstUp] uptime, the event is discarded. If the
+ * second down is not detected within [ViewConfiguration.doubleTapTimeoutMillis] of [firstUp],
+ * `null` is returned. Otherwise, the down event is returned.
+ */
+private suspend fun awaitChannelSecondDown(
+ channel: ReceiveChannel<TapGestureEvent>,
+ onlyDownsSignal: MutableState<Boolean>,
+ viewConfiguration: ViewConfiguration,
+ firstUp: Up
+): Down? {
+ return withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
+ val minUptime = firstUp.uptimeMillis + viewConfiguration.doubleTapMinTimeMillis
+ var change: Down
+ // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
+ do {
+ change = awaitChannelDown(onlyDownsSignal, channel)
+ } while (change.uptimeMillis < minUptime)
+ change
+ }
+}
+
+/**
+ * Special case to wait for all ups after long press has been fired. This sets a state value to
+ * true, indicating to the channel producer to consume all events until it will send an [AllUp]
+ * event. When all up happens and producer itself flips the value back to false, this method
+ * returns.
+ */
+private suspend fun awaitChannelAllUp(
+ consumeAllSignal: MutableState<Boolean>,
+ channel: ReceiveChannel<TapGestureEvent>
+) {
+ consumeAllSignal.value = true
+ var event = channel.receive()
+ while (event != AllUp) {
+ event = channel.receive()
+ }
+}
+
+/**
+ * Await up or cancel event from the channel and return either [Up] or [Cancel]
+ */
+private suspend fun awaitChannelUpOrCancel(
+ channel: ReceiveChannel<TapGestureEvent>
+): TapGestureEvent {
+ var event = channel.receive()
+ while (event !is Up && event !is Cancel) {
+ event = channel.receive()
+ }
+ return event
+}
+
+private sealed class TapGestureEvent {
+ class Down(val position: Offset, val uptimeMillis: Long) : TapGestureEvent()
+ class Up(val position: Offset, val uptimeMillis: Long) : TapGestureEvent()
+
+ // special case, the notification sent when we were consuming all previous events before all
+ // the pointers are up. AllUp means that we can restart the cycle after long press fired
+ object AllUp : TapGestureEvent()
+ object Cancel : TapGestureEvent()
+}
+
+/**
+ * Method to await domain specific [TapGestureEvent] from the [AwaitPointerEventScope] and send
+ * them to the specified [channel].
+ *
+ * Note: [consumeAllUntilUp] is a switch for a special case which happens when the long press has
+ * been fired, after which we want to block all the events until all fingers are up. This methods
+ * stars to consume all the events when [consumeAllUntilUp] is `true` and when all pointers are
+ * up it flips the [consumeAllUntilUp] itself, so it can suspend on the [AwaitPointerEventScope
+ * .awaitPointerEvent] again.
+ */
+private suspend fun AwaitPointerEventScope.translatePointerEventsToChannel(
+ scope: CoroutineScope,
+ channel: SendChannel<TapGestureEvent>,
+ detectDownsOnly: State<Boolean>,
+ consumeAllUntilUp: MutableState<Boolean>
+) {
+ while (scope.isActive) {
+ // operate normally, scan all downs / ups / cancels and push them to the channel
+ val event = awaitPointerEvent()
+ if (consumeAllUntilUp.value) {
+ event.changes.fastForEach { it.consumeAllChanges() }
+ // check the signal if we just need to consume everything on the initial pass for
+ // cases when the long press has fired and we block everything before all pointers
+ // are up
+ if (!allPointersUp()) {
+ do {
+ val initialEvent = awaitPointerEvent(PointerEventPass.Initial)
+ initialEvent.changes.fastForEach { it.consumeAllChanges() }
+ } while (initialEvent.changes.fastAny { it.pressed })
+ // wait for the main pass of the initial event we already have eaten above
+ awaitPointerEvent()
+ }
+ channel.offer(AllUp)
+ consumeAllUntilUp.value = false
+ } else if (event.changes.fastAll { it.changedToDown() }) {
+ val change = event.changes[0]
+ change.consumeDownChange()
+ channel.offer(Down(change.position, change.uptimeMillis))
+ } else if (!detectDownsOnly.value) {
+ if (event.changes.fastAll { it.changedToUp() }) {
+ // All pointers are up
+ val change = event.changes[0]
+ change.consumeDownChange()
+ channel.offer(Up(change.position, change.uptimeMillis))
+ } else if (
+ event.changes.fastAny { it.consumed.downChange || it.isOutOfBounds(size) }
+ ) {
+ channel.offer(Cancel)
+ } else {
+ // Check for cancel by position consumption. We can look on the Final pass of the
+ // existing pointer event because it comes after the Main pass we checked above.
+ val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
+ if (consumeCheck.changes.fastAny { it.positionChangeConsumed() }) {
+ channel.offer(Cancel)
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Shortcut for cases when we only need to get press/click logic, as for cases without long press
+ * and double click we don't require channelling or any other complications.
+ */
+internal suspend fun PointerInputScope.detectTapAndPress(
+ onPress: suspend PressGestureScope.(Offset) -> Unit = NoPressGesture,
+ onTap: ((Offset) -> Unit)? = null
+) {
+ val pressScope = PressGestureScopeImpl(this)
+ forEachGesture {
+ coroutineScope {
+ pressScope.reset()
+ awaitPointerEventScope {
+
+ val down = awaitFirstDown().also { it.consumeDownChange() }
+
+ if (onPress !== NoPressGesture) {
+ launch { pressScope.onPress(down.position) }
+ }
+
+ val up = waitForUpOrCancellation()
+ if (up == null) {
+ pressScope.cancel() // tap-up was canceled
+ } else {
+ up.consumeDownChange()
+ pressScope.release()
+ onTap?.invoke(up.position)
+ }
+ }
+ }
+ }
}
/**
@@ -224,42 +427,6 @@
}
/**
- * Consumes all event changes in the [PointerEventPass.Initial] until all pointers are up.
- */
-private suspend fun PointerInputScope.consumeAllEventsUntilUp() {
- awaitPointerEventScope {
- if (!allPointersUp()) {
- do {
- val event = awaitPointerEvent(PointerEventPass.Initial)
- event.changes.fastForEach { it.consumeAllChanges() }
- } while (event.changes.fastAny { it.pressed })
- }
- }
-}
-
-/**
- * Reads input for second tap down event. If the second tap is within
- * [ViewConfiguration.doubleTapMinTimeMillis] of [upTime], the event is discarded. If the second
- * down is not detected within [ViewConfiguration.doubleTapTimeoutMillis] of [upTime], `null` is
- * returned. Otherwise, the down event is returned.
- */
-private suspend fun PointerInputScope.detectSecondTapDown(
- upTime: Long
-): PointerInputChange? {
- return withTimeoutOrNull(viewConfiguration.doubleTapTimeoutMillis) {
- awaitPointerEventScope {
- val minUptime = upTime + viewConfiguration.doubleTapMinTimeMillis
- var change: PointerInputChange
- // The second tap doesn't count if it happens before DoubleTapMinTime of the first tap
- do {
- change = awaitFirstDown()
- } while (change.uptimeMillis < minUptime)
- change
- }
- }
-}
-
-/**
* [detectTapGestures]'s implementation of [PressGestureScope].
*/
private class PressGestureScopeImpl(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt
deleted file mode 100644
index b9e4ec3..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/DragGestureFilter.kt
+++ /dev/null
@@ -1,1047 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file: Suppress("DEPRECATION")
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.foundation.fastFilter
-import androidx.compose.foundation.fastFold
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.input.pointer.consumePositionChange
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.util.VelocityTracker
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlin.math.abs
-
-internal enum class Direction {
- LEFT, UP, RIGHT, DOWN
-}
-
-/**
- * Defines the callbacks associated with dragging.
- */
-@Deprecated("Use Modifier.pointerInput { detectDragGestures(...) }")
-internal interface DragObserver {
-
- /**
- * Override to be notified when a drag has started.
- *
- * This will be called as soon as the DragGestureDetector is allowed to start (canStartDragging
- * is null or returns true) and the average distance the pointers have moved are not 0 on
- * both the x and y axes.
- *
- * Only called if the last called if the most recent call among [onStart], [onStop], and
- * [onCancel] was [onStop] or [onCancel].
- *
- * @param downPosition The average position of all pointer positions when they first touched
- * down.
- */
- fun onStart(downPosition: Offset) {}
-
- /**
- * Override to be notified when a distance has been dragged.
- *
- * When overridden, return the amount of the [dragDistance] that has been consumed.
- *
- * Called immediately after [onStart] and for every subsequent pointer movement, as long as the
- * movement was enough to constitute a drag (the average movement on the x or y axis is not
- * equal to 0).
- *
- * Note: This may be called multiple times in a single pass and the values should be accumulated
- * for each call.
- *
- * @param dragDistance The distance that has been dragged. Reflects the average drag distance
- * of all pointers.
- */
- fun onDrag(dragDistance: Offset) = Offset.Zero
-
- /**
- * Override to be notified when a drag has stopped.
- *
- * This is called once all pointers have stopped interacting with this DragGestureDetector.
- *
- * Only called if the last called if the most recent call among [onStart], [onStop], and
- * [onCancel] was [onStart].
- *
- * @param velocity The velocity of the drag in both orientations at the point in time when all
- * pointers have released the relevant PointerInputFilter. In pixels per second.
- */
- fun onStop(velocity: Offset) {}
-
- /**
- * Override to be notified when the drag has been cancelled.
- *
- * This is called in response to a cancellation event such as the associated
- * PointerInputFilter having been removed from the hierarchy.
- *
- * Only called if the last called if the most recent call among [onStart], [onStop], and
- * [onCancel] was [onStart].
- */
- fun onCancel() {}
-}
-
-/**
- * Modeled after Android's ViewConfiguration:
- * https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
- */
-
-/** The time before a long press gesture attempts to win. */
-internal const val LongPressTimeoutMillis: Long = 500L
-
-/**
- * The maximum time from the start of the first tap to the start of the second
- * tap in a double-tap gesture.
- */
-// TODO(shepshapard): In Android, this is actually the time from the first's up event
-// to the second's down event, according to the ViewConfiguration docs.
-internal const val DoubleTapTimeoutMillis: Long = 300L
-
-/**
- * The distance a touch has to travel for the framework to be confident that
- * the gesture is a scroll gesture, or, inversely, the maximum distance that a
- * touch can travel before the framework becomes confident that it is not a
- * tap.
- */
-// This value was empirically derived. We started at 8.0 and increased it to
-// 18.0 after getting complaints that it was too difficult to hit targets.
-internal val TouchSlop = 18.dp
-
-// TODO(b/146133703): Likely rename to PanGestureDetector as per b/146133703
-/**
- * This gesture detector detects dragging in any direction.
- *
- * Dragging normally begins when the touch slop distance (currently defined by [TouchSlop]) is
- * surpassed in a supported direction (see [DragObserver.onDrag]). When dragging begins in this
- * manner, [DragObserver.onStart] is called, followed immediately by a call to
- * [DragObserver.onDrag]. [DragObserver.onDrag] is then continuously called whenever pointers
- * have moved. The gesture ends with either a call to [DragObserver.onStop] or
- * [DragObserver.onCancel], only after [DragObserver.onStart] is called. [DragObserver.onStop] is
- * called when the dragging ends due to all of the pointers no longer interacting with the
- * DragGestureDetector (for example, the last pointer has been lifted off of the
- * DragGestureDetector). [DragObserver.onCancel] is called when the dragging ends due to a system
- * cancellation event.
- *
- * If [startDragImmediately] is set to true, dragging will begin as soon as soon as a pointer comes
- * in contact with it, effectively ignoring touch slop and blocking any descendants from reacting
- * the "down" change. When dragging begins in this manner, [DragObserver.onStart] is called
- * immediately and is followed by [DragObserver.onDrag] when some drag distance has occurred.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param dragObserver The callback interface to report all events related to dragging.
- * @param startDragImmediately Set to true to have dragging begin immediately when a pointer is
- * "down", preventing children from responding to the "down" change. Generally, this parameter
- * should be set to true when the child of the GestureDetector is animating, such that when a finger
- * touches it, dragging is immediately started so the animation stops and dragging can occur.
- */
-@Deprecated("Use Modifier.pointerInput{ detectDragGestures(... )} instead")
-internal fun Modifier.dragGestureFilter(
- dragObserver: DragObserver,
- startDragImmediately: Boolean = false
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "dragGestureFilter"
- properties["dragObserver"] = dragObserver
- properties["startDragImmediately"] = startDragImmediately
- }
-) {
- val glue = remember { TouchSlopDragGestureDetectorGlue() }
- glue.touchSlopDragObserver = dragObserver
-
- // TODO(b/146427920): There is a gap here where RawPressStartGestureDetector can cause a call to
- // DragObserver.onStart but if the pointer doesn't move and releases, (or if cancel is called)
- // The appropriate callbacks to DragObserver will not be called.
- rawDragGestureFilter(glue.rawDragObserver, glue::enabledOrStarted)
- .dragSlopExceededGestureFilter(glue::enableDrag)
- .rawPressStartGestureFilter(
- glue::startDrag,
- startDragImmediately,
- PointerEventPass.Initial
- )
-}
-
-// TODO(shepshapard): Convert to functional component with effects once effects are ready.
-// TODO(shepshapard): Should this calculate the drag distance as the average of all fingers
-// (Shep thinks this is better), or should it only track the most recent finger to have
-// touched the screen over the detector (this is how Android currently does it)?
-// TODO(b/139020678): Probably has shared functionality with other movement based detectors.
-/**
- * This gesture detector detects dragging in any direction.
- *
- * Note: By default, this gesture detector only waits for a single pointer to have moved to start
- * dragging. It is extremely likely that you don't want to use this gesture detector directly, but
- * instead use a drag gesture detector that does wait for some other condition to have occurred
- * (such as [dragGestureFilter] which waits for a single pointer to have passed touch
- * slop before dragging starts).
- *
- * Dragging begins when the a single pointer has moved and either [canStartDragging] is null or
- * returns true. When dragging begins, [DragObserver.onStart] is called. [DragObserver.onDrag] is
- * then continuously called whenever the average movement of all pointers has movement along the x
- * or y axis. The gesture ends with either a call to [DragObserver.onStop] or
- * [DragObserver.onCancel], only after [DragObserver.onStart] is called. [DragObserver.onStop] is
- * called when the dragging ends due to all of the pointers no longer interacting with the
- * DragGestureDetector (for example, the last pointer has been lifted off of the
- * DragGestureDetector). [DragObserver.onCancel] is called when the dragging ends due to a system
- * cancellation event.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param dragObserver The callback interface to report all events related to dragging.
- * @param canStartDragging If set, Before dragging is started ([DragObserver.onStart] is called),
- * canStartDragging is called to check to see if it is allowed to start.
- */
-
-// TODO(b/129784010): Consider also allowing onStart, onDrag, and onStop to be set individually
-// (instead of all being set via DragObserver).
-@Deprecated("use Modifier.pointerInput { } with awaitFirstDown() and drag() functions")
-internal fun Modifier.rawDragGestureFilter(
- dragObserver: DragObserver,
- canStartDragging: (() -> Boolean)? = null,
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "rawDragGestureFilter"
- properties["dragObserver"] = dragObserver
- properties["canStartDragging"] = canStartDragging
- }
-) {
- val filter = remember { RawDragGestureFilter() }
- filter.dragObserver = dragObserver
- filter.canStartDragging = canStartDragging
- DragPointerInputModifierImpl(filter)
-}
-
-internal class RawDragGestureFilter : PointerInputFilter() {
- private val velocityTrackers: MutableMap<PointerId, VelocityTracker> = mutableMapOf()
- private val downPositions: MutableMap<PointerId, Offset> = mutableMapOf()
-
- internal lateinit var dragObserver: DragObserver
- internal var canStartDragging: (() -> Boolean)? = null
-
- private var started = false
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- val changes = pointerEvent.changes
-
- if (pass == PointerEventPass.Initial) {
- if (started) {
- // If we are have started we want to prevent any descendants from reacting to
- // any down change.
- changes.fastForEach {
- if (it.changedToDown()) {
- it.consumeDownChange()
- }
- }
- }
- }
-
- if (pass == PointerEventPass.Main) {
-
- // Get the changes for pointers that are relevant to us due to orientation locking.
-
- // Handle up changes, which includes removing individual pointer VelocityTrackers
- // and potentially calling onStop().
- if (changes.fastAny { it.changedToUpIgnoreConsumed() }) {
-
- // TODO(b/162269614): Should be update to only have one velocity tracker that
- // tracks the average change overtime, instead of one for each finger.
-
- var velocityTracker: VelocityTracker? = null
-
- changes.fastForEach {
- // This pointer is up (consumed or not), so we should stop tracking
- // information about it. If the pointer is not locked out of our
- // orientation, get the velocity tracker because this might be a fling.
- if (it.changedToUp() && changes.contains(it)) {
- velocityTracker = velocityTrackers.remove(it.id)
- } else if (it.changedToUpIgnoreConsumed()) {
- velocityTrackers.remove(it.id)
- }
- // removing stored down position for the pointer.
- if (it.changedToUp()) {
- downPositions.remove(it.id)
- }
- }
-
- if (changes.fastAll { it.changedToUpIgnoreConsumed() }) {
- // All of the pointers are up, so reset and call onStop. If we have a
- // velocityTracker at this point, that means at least one of the up events
- // was not consumed so we should send velocity for flinging.
- if (started) {
- val velocity: Offset? =
- if (velocityTracker != null) {
- changes.fastForEach {
- it.consumeDownChange()
- }
- val velocity = velocityTracker!!.calculateVelocity()
- Offset(velocity.x, velocity.y)
- } else {
- null
- }
- started = false
- dragObserver.onStop(velocity ?: Offset.Zero)
- reset()
- }
- }
- }
-
- // Handle down changes: for each new pointer that has been added, start tracking
- // information about it.
- if (changes.fastAny { it.changedToDownIgnoreConsumed() }) {
- changes.fastForEach {
- // If a pointer has changed to down, we should start tracking information
- // about it.
- if (it.changedToDownIgnoreConsumed()) {
- velocityTrackers[it.id] = VelocityTracker()
- .apply {
- addPosition(
- it.uptimeMillis,
- it.position
- )
- }
- downPositions[it.id] = it.position
- }
- }
- }
-
- // Handle moved changes.
-
- val movedChanges = changes.fastFilter {
- it.pressed && !it.changedToDownIgnoreConsumed()
- }
-
- movedChanges.fastForEach {
- // TODO(shepshapard): handle the case that the pointerTrackingData is null,
- // either with an exception or a logged error, or something else.
- // TODO(shepshapard): VelocityTracker needs to be updated to not accept
- // position information, but rather vector information about movement.
- // TODO(b/162269614): Should be update to only have one velocity tracker that
- // tracks the average change overtime, instead of one for each finger.
- velocityTrackers[it.id]?.addPosition(
- it.uptimeMillis,
- it.position
- )
- }
-
- // Check to see if we are already started so we don't have to call canStartDragging
- // again.
- val canStart = !started && canStartDragging?.invoke() ?: true
-
- // At this point, check to see if we have started, and if we have, we may
- // be calling onDrag and updating change information on the PointerInputChanges.
- if (started || canStart) {
-
- var totalDx = 0f
- var totalDy = 0f
-
- movedChanges.fastForEach {
- if (movedChanges.contains(it)) {
- totalDx += it.positionChange().x
- }
- if (movedChanges.contains(it)) {
- totalDy += it.positionChange().y
- }
- }
-
- if (totalDx != 0f || totalDy != 0f) {
-
- // At this point, if we have not started, check to see if we should start
- // and if we should, update our state and call onStart().
- if (!started) {
- started = true
- dragObserver.onStart(downPositions.values.averagePosition())
- downPositions.clear()
- }
-
- val consumed = dragObserver.onDrag(
- Offset(
- totalDx / changes.size,
- totalDy / changes.size
- )
- )
-
- if (consumed.x != 0f || consumed.y != 0f) {
- movedChanges.fastForEach { it.consumePositionChange() }
- }
- }
- }
- }
- }
-
- override fun onCancel() {
- downPositions.clear()
- velocityTrackers.clear()
- if (started) {
- started = false
- dragObserver.onCancel()
- }
- reset()
- }
-
- private fun reset() {
- downPositions.clear()
- velocityTrackers.clear()
- }
-}
-
-private fun Iterable<Offset>.averagePosition(): Offset {
- var x = 0f
- var y = 0f
- this.forEach {
- x += it.x
- y += it.y
- }
- return Offset(x / count(), y / count())
-}
-
-/**
- * Glues together the logic of RawDragGestureDetector, TouchSlopExceededGestureDetector, and
- * InterruptFlingGestureDetector.
- */
-private class TouchSlopDragGestureDetectorGlue {
-
- lateinit var touchSlopDragObserver: DragObserver
- var started = false
- var enabled = false
- val enabledOrStarted
- get() = started || enabled
-
- fun enableDrag() {
- enabled = true
- }
-
- fun startDrag(downPosition: Offset) {
- started = true
- touchSlopDragObserver.onStart(downPosition)
- }
-
- val rawDragObserver: DragObserver =
- object : DragObserver {
- override fun onStart(downPosition: Offset) {
- if (!started) {
- touchSlopDragObserver.onStart(downPosition)
- }
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- return touchSlopDragObserver.onDrag(dragDistance)
- }
-
- override fun onStop(velocity: Offset) {
- started = false
- enabled = false
- touchSlopDragObserver.onStop(velocity)
- }
-
- override fun onCancel() {
- started = false
- enabled = false
- touchSlopDragObserver.onCancel()
- }
- }
-}
-
-/**
- * Reacts if the first pointer input change it sees is an unconsumed down change, and if it reacts,
- * consumes all further down changes.
- *
- * This GestureDetector is not generally intended to be used directly, but is instead intended to be
- * used as a building block to create more complex GestureDetectors.
- *
- * This GestureDetector is a bit more experimental then the other GestureDetectors (the number and
- * types of GestureDetectors is still very much a work in progress) and is intended to be a
- * generically useful building block for more complicated GestureDetectors.
- *
- * The theory is that this GestureDetector can be reused in PressIndicatorGestureDetector, and there
- * could be a corresponding RawPressReleasedGestureDetector.
- *
- * @param onPressStart Called when the first pointer "presses" on the GestureDetector. [Offset]
- * is the position of that first pointer on press.
- * @param enabled If false, this GestureDetector will effectively act as if it is not in the
- * hierarchy.
- * @param executionPass The [PointerEventPass] during which this GestureDetector will attempt to
- * react to and consume down changes. Defaults to [PointerEventPass.Main].
- */
-@Deprecated("Use Modifier.pointerInput{} with custom gesture detection code")
-internal fun Modifier.rawPressStartGestureFilter(
- onPressStart: (Offset) -> Unit,
- enabled: Boolean = false,
- executionPass: PointerEventPass = PointerEventPass.Main
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "rawPressStartGestureFilter"
- properties["onPressStart"] = onPressStart
- properties["enabled"] = enabled
- properties["executionPass"] = executionPass
- }
-) {
- val filter = remember { RawPressStartGestureFilter() }
- filter.onPressStart = onPressStart
- filter.setEnabled(enabled = enabled)
- filter.setExecutionPass(executionPass)
- DragPointerInputModifierImpl(filter)
-}
-
-internal class RawPressStartGestureFilter : PointerInputFilter() {
-
- lateinit var onPressStart: (Offset) -> Unit
- private var enabled: Boolean = true
- private var executionPass = PointerEventPass.Initial
-
- private var active = false
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- val changes = pointerEvent.changes
-
- if (pass == executionPass) {
- if (enabled && changes.fastAll { it.changedToDown() }) {
- // If we have not yet started and all of the changes changed to down, we are
- // starting.
- active = true
- onPressStart(changes.first().position)
- } else if (changes.fastAll { it.changedToUp() }) {
- // If we have started and all of the changes changed to up, we are stopping.
- active = false
- }
-
- if (active) {
- // If we have started, we should consume the down change on all changes.
- changes.fastForEach {
- it.consumeDownChange()
- }
- }
- }
- }
-
- override fun onCancel() {
- active = false
- }
-
- fun setEnabled(enabled: Boolean) {
- this.enabled = enabled
- // Whenever we are disabled, we can just go ahead and become inactive (which is the state we
- // should be in if we are to pretend that we aren't in the hierarchy.
- if (!enabled) {
- onCancel()
- }
- }
-
- fun setExecutionPass(executionPass: PointerEventPass) {
- this.executionPass = executionPass
- }
-}
-
-internal fun Modifier.dragSlopExceededGestureFilter(
- onDragSlopExceeded: () -> Unit
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "dragSlopExceededGestureFilter"
- properties["onDragSlopExceeded"] = onDragSlopExceeded
- }
-) {
- val touchSlop = with(LocalDensity.current) { TouchSlop.toPx() }
- val filter = remember {
- DragSlopExceededGestureFilter(touchSlop)
- }
- filter.onDragSlopExceeded = onDragSlopExceeded
- DragPointerInputModifierImpl(filter)
-}
-
-internal class DragSlopExceededGestureFilter(
- private val touchSlop: Float
-) : PointerInputFilter() {
- private var dxForPass = 0f
- private var dyForPass = 0f
- private var dxUnderSlop = 0f
- private var dyUnderSlop = 0f
- private var passedSlop = false
-
- private var canDrag: ((Direction) -> Boolean)? = null
-
- var onDragSlopExceeded: () -> Unit = {}
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
-
- val changes = pointerEvent.changes
-
- if (pass == PointerEventPass.Main || pass == PointerEventPass.Final) {
-
- // Filter changes for those that we can interact with due to our orientation.
-
- if (!passedSlop) {
-
- // Get current average change.
- val averagePositionChange = getAveragePositionChange(changes)
- val dx = averagePositionChange.x
- val dy = averagePositionChange.y
-
- // Track changes during main and during final. This allows for fancy dragging
- // due to a parent being dragged and will likely be removed.
- // TODO(b/157087973): Likely remove this two pass complexity.
- if (pass == PointerEventPass.Main) {
- dxForPass = dx
- dyForPass = dy
- dxUnderSlop += dx
- dyUnderSlop += dy
- } else {
- dxUnderSlop += dx - dxForPass
- dyUnderSlop += dy - dyForPass
- }
-
- // Map the distance to the direction enum for a call to canDrag.
- val directionX = averagePositionChange.horizontalDirection()
- val directionY = averagePositionChange.verticalDirection()
-
- val canDragX = directionX != null && canDrag?.invoke(directionX) ?: true
- val canDragY = directionY != null && canDrag?.invoke(directionY) ?: true
-
- val passedSlopX = canDragX && abs(dxUnderSlop) > touchSlop
- val passedSlopY = canDragY && abs(dyUnderSlop) > touchSlop
-
- if (passedSlopX || passedSlopY) {
- passedSlop = true
- onDragSlopExceeded.invoke()
- } else {
- // If we have passed slop in a direction that we can't drag in, we should reset
- // our tracking back to zero so that a user doesn't have to later scroll the slop
- // + the extra distance they scrolled in the wrong direction.
- if (!canDragX &&
- (
- (directionX == Direction.LEFT && dxUnderSlop < 0) ||
- (directionX == Direction.RIGHT && dxUnderSlop > 0)
- )
- ) {
- dxUnderSlop = 0f
- }
- if (!canDragY &&
- (
- (directionY == Direction.UP && dyUnderSlop < 0) ||
- (directionY == Direction.DOWN && dyUnderSlop > 0)
- )
- ) {
- dyUnderSlop = 0f
- }
- }
- }
-
- if (pass == PointerEventPass.Final &&
- changes.fastAll { it.changedToUpIgnoreConsumed() }
- ) {
- // On the final pass, check to see if all pointers have changed to up, and if they
- // have, reset.
- reset()
- }
- }
- }
-
- override fun onCancel() {
- reset()
- }
-
- private fun reset() {
- passedSlop = false
- dxForPass = 0f
- dyForPass = 0f
- dxUnderSlop = 0f
- dyUnderSlop = 0f
- }
-}
-
-/**
- * Gets the average distance change of all pointers as an Offset.
- */
-private fun getAveragePositionChange(changes: List<PointerInputChange>): Offset {
- if (changes.isEmpty()) {
- return Offset.Zero
- }
-
- val sum = changes.fastFold(Offset.Zero) { sum, change ->
- sum + change.positionChange()
- }
- val sizeAsFloat = changes.size.toFloat()
- // TODO(b/148980115): Once PxPosition is removed, sum will be an Offset, and this line can
- // just be straight division.
- return Offset(sum.x / sizeAsFloat, sum.y / sizeAsFloat)
-}
-
-/**
- * Maps an [Offset] value to a horizontal [Direction].
- */
-private fun Offset.horizontalDirection() =
- when {
- this.x < 0f -> Direction.LEFT
- this.x > 0f -> Direction.RIGHT
- else -> null
- }
-
-/**
- * Maps a [Offset] value to a vertical [Direction].
- */
-private fun Offset.verticalDirection() =
- when {
- this.y < 0f -> Direction.UP
- this.y > 0f -> Direction.DOWN
- else -> null
- }
-
-private data class DragPointerInputModifierImpl(
- override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
-
-@Deprecated("Use Modifier.pointerInput {detectDragGesturesAfterLongPress(...)} instead")
-internal interface LongPressDragObserver {
-
- /**
- * Override to be notified when a long press has occurred and thus dragging can start.
- *
- * Note that when this is called, dragging hasn't actually started, but rather, dragging can start. When dragging
- * has actually started, [onDragStart] will be called. It is possible for [onDragStart] to be called immediately
- * after this synchronously in the same event stream.
- *
- * This won't be called again until after [onStop] has been called.
- *
- * @see onDragStart
- * @see onDrag
- * @see onStop
- */
- fun onLongPress(pxPosition: Offset) {}
-
- /**
- * Override to be notified when dragging has actually begun.
- *
- * Dragging has begun when both [onLongPress] has been called, and the average pointer distance change is not 0.
- *
- * This will not be called until after [onLongPress] has been called, and may be called synchronously,
- * immediately afterward [onLongPress], as a result of the same pointer input event.
- *
- * This will not be called again until [onStop] has been called.
- *
- * @see onLongPress
- * @see onDrag
- * @see onStop
- */
- fun onDragStart() {}
-
- /**
- * Override to be notified when a distance has been dragged.
- *
- * When overridden, return the amount of the [dragDistance] that has been consumed.
- *
- * Called after [onDragStart] and for every subsequent pointer movement, as long as the movement
- * was enough to constitute a drag (the average movement on the x or y axis is not equal to
- * 0). This may be called synchronously, immediately afterward [onDragStart], as a result of
- * the same pointer input event.
- *
- * Note: This will be called multiple times for a single pointer input event and the values
- * provided in each call should be considered unique.
- *
- * @param dragDistance The distance that has been dragged. Reflects the average drag distance
- * of all pointers.
- */
- fun onDrag(dragDistance: Offset) = Offset.Zero
-
- /**
- * Override to be notified when a drag has stopped.
- *
- * This is called once all pointers have stopped interacting with this DragGestureDetector and
- * [onLongPress] was previously called.
- */
- fun onStop(velocity: Offset) {}
-
- /**
- * Override to be notified when the drag has been cancelled.
- *
- * This is called if [onLongPress] has ben called and then a cancellation event has occurs
- * (for example, due to the gesture detector being removed from the tree) before [onStop] is
- * called.
- */
- fun onCancel() {}
-}
-
-/**
- * This gesture detector detects dragging in any direction, but only after a long press has first
- * occurred.
- *
- * Dragging begins once a long press has occurred and then dragging occurs. When long press occurs,
- * [LongPressDragObserver.onLongPress] is called. Once dragging has occurred,
- * [LongPressDragObserver.onDragStart] will be called. [LongPressDragObserver.onDrag] is then
- * continuously called whenever pointer movement results in a drag. The gesture will end
- * with either a call to [LongPressDragObserver.onStop] or [LongPressDragObserver.onCancel]. Either
- * will be called after [LongPressDragObserver.onLongPress] is called.
- * [LongPressDragObserver.onStop] is called when the gesture ends due to all of the pointers
- * no longer interacting with the LongPressDragGestureDetector (for example, the last finger has
- * been lifted off of the LongPressDragGestureDetector). [LongPressDragObserver.onCancel] is
- * called in response to a system cancellation event.
- *
- * When multiple pointers are touching the detector, the drag distance is taken as the average of
- * all of the pointers.
- *
- * @param longPressDragObserver The callback interface to report all events.
- * @see LongPressDragObserver
- */
-@Deprecated("Use Modifier.pointerInput { detectDragGesturesAfterLongPress(...)} instead.")
-internal fun Modifier.longPressDragGestureFilter(
- longPressDragObserver: LongPressDragObserver
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "longPressDragGestureFilter"
- properties["longPressDragObserver"] = longPressDragObserver
- }
-) {
- val glue = remember { LongPressDragGestureDetectorGlue() }
- glue.longPressDragObserver = longPressDragObserver
-
- rawDragGestureFilter(glue.dragObserver, glue::dragEnabled)
- .then(LongPointerInputModifierImpl(glue))
- .longPressGestureFilter(glue.onLongPress)
-}
-
-private class LongPressDragGestureDetectorGlue : PointerInputFilter() {
- lateinit var longPressDragObserver: LongPressDragObserver
- private var dragStarted: Boolean = false
- var dragEnabled: Boolean = false
-
- val dragObserver: DragObserver =
-
- object : DragObserver {
-
- override fun onStart(downPosition: Offset) {
- longPressDragObserver.onDragStart()
- dragStarted = true
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- return longPressDragObserver.onDrag(dragDistance)
- }
-
- override fun onStop(velocity: Offset) {
- dragEnabled = false
- dragStarted = false
- longPressDragObserver.onStop(velocity)
- }
-
- override fun onCancel() {
- dragEnabled = false
- dragStarted = false
- longPressDragObserver.onCancel()
- }
- }
-
- // This handler ensures that onStop will be called after long press happened, but before
- // dragging actually has begun.
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- if (pass == PointerEventPass.Main &&
- dragEnabled &&
- !dragStarted &&
- pointerEvent.changes.fastAll { it.changedToUpIgnoreConsumed() }
- ) {
- dragEnabled = false
- longPressDragObserver.onStop(Offset.Zero)
- }
- }
-
- // This handler ensures that onCancel is called if onLongPress was previously called but
- // dragging has not yet started.
- override fun onCancel() {
- if (dragEnabled && !dragStarted) {
- dragEnabled = false
- longPressDragObserver.onCancel()
- }
- }
-
- val onLongPress = { pxPosition: Offset ->
- dragEnabled = true
- longPressDragObserver.onLongPress(pxPosition)
- }
-}
-
-internal fun Modifier.longPressGestureFilter(
- onLongPress: (Offset) -> Unit
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "longPressGestureFilter"
- properties["onLongPress"] = onLongPress
- }
-) {
- @Suppress("DEPRECATION")
- val scope = rememberCoroutineScope()
- val filter = remember { LongPressGestureFilter(scope) }
- filter.onLongPress = onLongPress
- LongPointerInputModifierImpl(filter)
-}
-
-internal class LongPressGestureFilter(
- private val coroutineScope: CoroutineScope
-) : PointerInputFilter() {
- lateinit var onLongPress: (Offset) -> Unit
-
- /*@VisibleForTesting*/
- internal var longPressTimeout = LongPressTimeoutMillis
-
- private enum class State {
- Idle, Primed, Fired
- }
-
- private var state = State.Idle
- private val pointerPositions = linkedMapOf<PointerId, Offset>()
- private var job: Job? = null
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- val changes = pointerEvent.changes
-
- if (pass == PointerEventPass.Initial) {
- if (state == State.Fired) {
- // If we fired and have not reset, we should prevent other pointer input nodes from
- // responding to up, so consume it early on.
- changes.fastForEach {
- if (it.changedToUp()) {
- it.consumeDownChange()
- }
- }
- }
- return
- }
-
- if (pass == PointerEventPass.Main) {
- if (state == State.Idle && changes.fastAll { it.changedToDown() }) {
- // If we are idle and all of the changes changed to down, we are prime to fire
- // the event.
- primeToFire()
- } else if (state != State.Idle && changes.fastAll { it.changedToUpIgnoreConsumed() }) {
- // If we have started and all of the changes changed to up, reset to idle.
- resetToIdle()
- } else if (!changes.anyPointersInBounds(bounds)) {
- // If all pointers have gone out of bounds, reset to idle.
- resetToIdle()
- }
-
- if (state == State.Primed) {
- // If we are primed, keep track of all down pointer positions so we can pass
- // pointer position information to the event we will fire.
- changes.fastForEach {
- if (it.pressed) {
- pointerPositions[it.id] = it.position
- } else {
- pointerPositions.remove(it.id)
- }
- }
- }
- }
-
- if (
- pass == PointerEventPass.Final &&
- state != State.Idle &&
- changes.fastAny { it.positionChangeConsumed() }
- ) {
- // If we are anything but Idle and something consumed movement, reset.
- resetToIdle()
- }
- }
-
- // TODO(shepshapard): This continues to be very confusing to use. Have to come up with a better
-// way of easily expressing this.
- /**
- * Utility method that determines if any pointers are currently in [bounds].
- *
- * A pointer is considered in bounds if it is currently down and it's current
- * position is within the provided [bounds]
- *
- * @return True if at least one pointer is in bounds.
- */
- private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
- fastAny {
- it.pressed &&
- it.position.x >= 0 &&
- it.position.x < bounds.width &&
- it.position.y >= 0 &&
- it.position.y < bounds.height
- }
-
- override fun onCancel() {
- resetToIdle()
- }
-
- private fun fireLongPress() {
- state = State.Fired
- onLongPress.invoke(pointerPositions.asIterable().first().value)
- }
-
- private fun primeToFire() {
- state = State.Primed
- job = coroutineScope.launch {
- delay(longPressTimeout)
- fireLongPress()
- }
- }
-
- private fun resetToIdle() {
- state = State.Idle
- job?.cancel()
- pointerPositions.clear()
- }
-}
-
-private data class LongPointerInputModifierImpl(
- override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt
deleted file mode 100644
index ccf2dbf..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/PressIndicatorGestureFilter.kt
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-
-/**
- * This gesture detector has callbacks for when a press gesture starts and ends for the purposes of
- * displaying visual feedback for those two states.
- *
- * More specifically:
- * - It will call [onStart] if the first pointer down it receives during the
- * [PointerEventPass.Main] pass is not consumed.
- * - It will call [onStop] if [onStart] has been called and the last [PointerInputChange] it
- * receives during the [PointerEventPass.Main] pass has an up change, consumed or not, indicating
- * the press gesture indication should end.
- * - It will call [onCancel] if movement has been consumed by the time of the
- * [PointerEventPass.Final] pass, indicating that the press gesture indication should end because
- * something moved.
- *
- * This gesture detector always consumes the down change during the [PointerEventPass.Main] pass.
- */
-// TODO(b/139020678): Probably has shared functionality with other press based detectors.
-@Deprecated(
- "Gesture filters are deprecated. Use Modifier.clickable or Modifier.pointerInput and " +
- "detectTapGestures instead",
- replaceWith = ReplaceWith(
- """
- pointerInput {
- detectTapGestures(onPress = {
- onStart?.invoke(it)
- val success = tryAwaitRelease()
- if (success) {
- onStop?.invoke()
- } else {
- onCancel?.invoke()
- }
- })
- }""",
- "androidx.compose.ui.input.pointer.pointerInput",
- "androidx.compose.foundation.gestures.detectTapGestures"
- )
-)
-internal fun Modifier.pressIndicatorGestureFilter(
- onStart: ((Offset) -> Unit)? = null,
- onStop: (() -> Unit)? = null,
- onCancel: (() -> Unit)? = null,
- enabled: Boolean = true
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "pressIndicatorGestureFilter"
- properties["onStart"] = onStart
- properties["onStop"] = onStop
- properties["onCancel"] = onCancel
- properties["enabled"] = enabled
- }
-) {
- val filter = remember { PressIndicatorGestureFilter() }
- filter.onStart = onStart
- filter.onStop = onStop
- filter.onCancel = onCancel
- filter.setEnabled(enabled)
- PressPointerInputModifierImpl(filter)
-}
-
-internal class PressIndicatorGestureFilter : PointerInputFilter() {
- /**
- * Called if the first pointer's down change was not consumed by the time this gesture
- * filter receives it in the [PointerEventPass.Main] pass.
- *
- * This callback should be used to indicate that the press state should be shown. An [Offset]
- * is provided to indicate where the first pointer made contact with this gesrure detector.
- */
- var onStart: ((Offset) -> Unit)? = null
-
- /**
- * Called if onStart was attempted to be called (it may have been null), no pointer movement
- * was consumed, and the last pointer went up (consumed or not).
- *
- * This should be used for removing visual feedback that indicates that the press has ended with
- * a completed press released gesture.
- */
- var onStop: (() -> Unit)? = null
-
- /**
- * Called if onStart was attempted to be called (it may have been null), and either:
- * 1. Pointer movement was consumed by the time [PointerEventPass.Final] reaches this
- * gesture filter.
- * 2. [setEnabled] is called with false.
- * 3. This [PointerInputFilter] is removed from the hierarchy, or it has no descendants
- * to define it's position or size.
- * 4. The Compose root is notified that it will no longer receive input, and thus onStop
- * will never be reached (For example, the Android View that hosts compose receives
- * MotionEvent.ACTION_CANCEL).
- *
- * This should be used for removing visual feedback that indicates that the press gesture was
- * cancelled.
- */
- var onCancel: (() -> Unit)? = null
-
- private var state = State.Idle
-
- /**
- * Sets whether this [PointerInputFilter] is enabled. True by default.
- *
- * When enabled, this [PointerInputFilter] will act normally.
- *
- * When disabled, this [PointerInputFilter] will not process any input. No aspects
- * of any [PointerInputChange]s will be consumed and no callbacks will be called.
- *
- * If the last callback that was attempted to be called was [onStart] ([onStart] may have
- * been false) and [enabled] is false, [onCancel] will be called.
- */
- // TODO(shepshapard): Remove 'setEnabled'. It serves no purpose anymore.
- fun setEnabled(enabled: Boolean) {
- if (state == State.Started) {
- // If the state is Started and we were passed true, we don't want to change it to
- // Enabled.
- // If the state is Started and we were passed false, we can set to Disabled and
- // call the cancel callback.
- if (!enabled) {
- state = State.Disabled
- onCancel?.invoke()
- }
- } else {
- // If the state is anything but Started, just set the state according to the value
- // we were passed.
- state =
- if (enabled) {
- State.Idle
- } else {
- State.Disabled
- }
- }
- }
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- val changes = pointerEvent.changes
-
- if (pass == PointerEventPass.Initial && state == State.Started) {
- changes.fastForEach {
- if (it.changedToDown()) {
- it.consumeDownChange()
- }
- }
- }
-
- if (pass == PointerEventPass.Main) {
-
- if (state == State.Idle && changes.fastAll { it.changedToDown() }) {
- // If we have not yet started and all of the changes changed to down, we are
- // starting.
- state = State.Started
- onStart?.invoke(changes.first().position)
- } else if (state == State.Started) {
- if (changes.fastAll { it.changedToUpIgnoreConsumed() }) {
- // If we have started and all of the changes changed to up, we are stopping.
- state = State.Idle
- onStop?.invoke()
- } else if (!changes.anyPointersInBounds(bounds)) {
- // If all of the down pointers are currently out of bounds, we should cancel
- // as this indicates that the user does not which to trigger a press based
- // event.
- state = State.Idle
- onCancel?.invoke()
- }
- }
-
- if (state == State.Started) {
- changes.fastForEach {
- it.consumeDownChange()
- }
- }
- }
-
- if (
- pass == PointerEventPass.Final &&
- state == State.Started &&
- changes.fastAny { it.positionChangeConsumed() }
- ) {
- // On the final pass, if we have started and any of the changes had consumed
- // position changes, we cancel.
- state = State.Idle
- onCancel?.invoke()
- }
- }
-
- // TODO(shepshapard): This continues to be very confusing to use. Have to come up with a better
-// way of easily expressing this.
- /**
- * Utility method that determines if any pointers are currently in [bounds].
- *
- * A pointer is considered in bounds if it is currently down and it's current
- * position is within the provided [bounds]
- *
- * @return True if at least one pointer is in bounds.
- */
- private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
- fastAny {
- it.pressed &&
- it.position.x >= 0 &&
- it.position.x < bounds.width &&
- it.position.y >= 0 &&
- it.position.y < bounds.height
- }
-
- override fun onCancel() {
- if (state == State.Started) {
- state = State.Idle
- onCancel?.invoke()
- }
- }
-
- private enum class State {
- Disabled, Idle, Started
- }
-}
-
-private data class PressPointerInputModifierImpl(
- override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt
deleted file mode 100644
index 06e73d2..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/legacygestures/TapGestureFilter.kt
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.legacygestures
-
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.PointerEvent
-import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputFilter
-import androidx.compose.ui.input.pointer.PointerInputModifier
-import androidx.compose.ui.input.pointer.positionChangeConsumed
-import androidx.compose.ui.input.pointer.changedToDown
-import androidx.compose.ui.input.pointer.changedToUp
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
-import androidx.compose.ui.input.pointer.consumeDownChange
-import androidx.compose.ui.platform.debugInspectorInfo
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastAll
-import androidx.compose.ui.util.fastAny
-import androidx.compose.ui.util.fastForEach
-
-/**
- * This gesture detector fires a callback when a traditional press is being released. This is
- * generally the same thing as "onTap" or "onClick".
- *
- * [onTap] is called with the position of the last pointer to go "up".
- *
- * More specifically, it will call [onTap] if:
- * - All of the first [PointerInputChange]s it receives during the [PointerEventPass.Main] pass
- * have unconsumed down changes, thus representing new set of pointers, none of which have had
- * their down events consumed.
- * - The last [PointerInputChange] it receives during the [PointerEventPass.Main] pass has
- * an unconsumed up change.
- * - While it has at least one pointer touching it, no [PointerInputChange] has had any
- * movement consumed (as that would indicate that something in the heirarchy moved and this a
- * press should be cancelled.
- *
- * @param onTap Called when a tap has occurred.
- */
-// TODO(b/139020678): Probably has shared functionality with other press based detectors.
-@Deprecated(
- "Gesture filters are deprecated. Use Modifier.clickable or Modifier.pointerInput and " +
- "detectTapGestures instead",
- replaceWith = ReplaceWith(
- """pointerInput { detectTapGestures(onTap = onTap)}""",
- "androidx.compose.ui.input.pointer.pointerInput",
- "androidx.compose.foundation.gestures.detectTapGestures"
- )
-)
-internal fun Modifier.tapGestureFilter(
- onTap: (Offset) -> Unit
-): Modifier = composed(
- inspectorInfo = debugInspectorInfo {
- name = "tapGestureFilter"
- this.properties["onTap"] = onTap
- }
-) {
- val filter = remember { TapGestureFilter() }
- filter.onTap = onTap
- TapPointerInputModifierImpl(filter)
-}
-
-internal class TapGestureFilter : PointerInputFilter() {
- /**
- * Called to indicate that a press gesture has successfully completed.
- *
- * This should be used to fire a state changing event as if a button was pressed.
- */
- lateinit var onTap: (Offset) -> Unit
-
- /**
- * Whether or not to consume changes.
- */
- var consumeChanges: Boolean = true
-
- /**
- * True when we are primed to call [onTap] and may be consuming all down changes.
- */
- private var primed = false
-
- private var downPointers: MutableSet<PointerId> = mutableSetOf()
- private var upBlockedPointers: MutableSet<PointerId> = mutableSetOf()
- private var lastPxPosition: Offset? = null
-
- override fun onPointerEvent(
- pointerEvent: PointerEvent,
- pass: PointerEventPass,
- bounds: IntSize
- ) {
- val changes = pointerEvent.changes
-
- if (pass == PointerEventPass.Main) {
-
- if (primed &&
- changes.fastAll { it.changedToUp() }
- ) {
- val pointerPxPosition: Offset = changes[0].previousPosition
- if (changes.fastAny { !upBlockedPointers.contains(it.id) }) {
- // If we are primed, all pointers went up, and at least one of the pointers is
- // not blocked, we can fire, reset, and consume all of the up events.
- reset()
- onTap.invoke(pointerPxPosition)
- if (consumeChanges) {
- changes.fastForEach {
- it.consumeDownChange()
- }
- }
- return
- } else {
- lastPxPosition = pointerPxPosition
- }
- }
-
- if (changes.fastAll { it.changedToDown() }) {
- // Reset in case we were incorrectly left waiting on a delayUp message.
- reset()
- // If all of the changes are down, can become primed.
- primed = true
- }
-
- if (primed) {
- changes.fastForEach {
- if (it.changedToDown()) {
- downPointers.add(it.id)
- }
- if (it.changedToUpIgnoreConsumed()) {
- downPointers.remove(it.id)
- }
- }
- }
- }
-
- if (pass == PointerEventPass.Final && primed) {
-
- val anyPositionChangeConsumed = changes.fastAny { it.positionChangeConsumed() }
-
- val noPointersInBounds =
- upBlockedPointers.isEmpty() && !changes.anyPointersInBounds(bounds)
-
- if (anyPositionChangeConsumed || noPointersInBounds) {
- // If we are on the final pass, we are primed, and either we aren't blocked and
- // all pointers are out of bounds.
- reset()
- }
- }
- }
-
- // TODO(shepshapard): This continues to be very confusing to use. Have to come up with a better
-// way of easily expressing this.
- /**
- * Utility method that determines if any pointers are currently in [bounds].
- *
- * A pointer is considered in bounds if it is currently down and it's current
- * position is within the provided [bounds]
- *
- * @return True if at least one pointer is in bounds.
- */
- private fun List<PointerInputChange>.anyPointersInBounds(bounds: IntSize) =
- fastAny {
- it.pressed &&
- it.position.x >= 0 &&
- it.position.x < bounds.width &&
- it.position.y >= 0 &&
- it.position.y < bounds.height
- }
-
- override fun onCancel() {
- reset()
- }
-
- private fun reset() {
- primed = false
- upBlockedPointers.clear()
- downPointers.clear()
- lastPxPosition = null
- }
-}
-
-private data class TapPointerInputModifierImpl(
- override val pointerInputFilter: PointerInputFilter
-) : PointerInputModifier
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
index 9e500518..fb7ce95 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/selection/Toggleable.kt
@@ -17,19 +17,20 @@
package androidx.compose.foundation.selection
import androidx.compose.foundation.Indication
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.PressedInteractionSourceDisposableEffect
import androidx.compose.foundation.LocalIndication
-import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.Strings
+import androidx.compose.foundation.gestures.detectTapAndPress
+import androidx.compose.foundation.handlePressInteraction
import androidx.compose.foundation.indication
-import androidx.compose.runtime.DisposableEffect
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.disabled
@@ -42,7 +43,6 @@
import androidx.compose.ui.state.ToggleableState.Indeterminate
import androidx.compose.ui.state.ToggleableState.Off
import androidx.compose.ui.state.ToggleableState.On
-import kotlinx.coroutines.launch
/**
* Configure component to make it toggleable via input and accessibility events
@@ -233,7 +233,7 @@
}
)
-@Suppress("ModifierInspectorInfo", "DEPRECATION")
+@Suppress("ModifierInspectorInfo")
private fun Modifier.toggleableImpl(
state: ToggleableState,
enabled: Boolean,
@@ -242,7 +242,6 @@
indication: Indication?,
onClick: () -> Unit
): Modifier = composed {
- val scope = rememberCoroutineScope()
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
// TODO(pavlis): Handle multiple states for Semantics
val semantics = Modifier.semantics(mergeDescendants = true) {
@@ -261,58 +260,22 @@
disabled()
}
}
- val interactionUpdate =
- if (enabled) {
- Modifier.pressIndicatorGestureFilter(
- onStart = {
- scope.launch {
- // Remove any old interactions if we didn't fire stop / cancel properly
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- val interaction = PressInteraction.Press(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = interaction
- }
+ val onClickState = rememberUpdatedState(onClick)
+ val gestures = if (enabled) {
+ PressedInteractionSourceDisposableEffect(interactionSource, pressedInteraction)
+ Modifier.pointerInput(interactionSource) {
+ detectTapAndPress(
+ onPress = { offset ->
+ handlePressInteraction(offset, interactionSource, pressedInteraction)
},
- onStop = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Release(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- }
- },
- onCancel = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Cancel(it)
- interactionSource.emit(interaction)
- pressedInteraction.value = null
- }
- }
- }
+ onTap = { onClickState.value.invoke() }
)
- } else {
- Modifier
}
- val click = if (enabled) Modifier.tapGestureFilter { onClick() } else Modifier
-
- DisposableEffect(interactionSource) {
- onDispose {
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
- interactionSource.tryEmit(interaction)
- pressedInteraction.value = null
- }
- }
+ } else {
+ Modifier
}
this
.then(semantics)
.indication(interactionSource, indication)
- .then(interactionUpdate)
- .then(click)
+ .then(gestures)
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 4b2e134..2f03a87 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -18,21 +18,26 @@
package androidx.compose.foundation.text
import androidx.compose.foundation.fastMapIndexedNotNull
-import androidx.compose.foundation.legacygestures.DragObserver
+import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
+import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.MultiWidgetSelectionDelegate
+import androidx.compose.foundation.text.selection.Selectable
+import androidx.compose.foundation.text.selection.SelectionRegistrar
+import androidx.compose.foundation.text.selection.hasSelection
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.DisposableEffectResult
import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.longPressDragGestureFilter
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.FirstBaseline
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
@@ -47,13 +52,6 @@
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFontLoader
-import androidx.compose.foundation.text.selection.LocalSelectionRegistrar
-import androidx.compose.foundation.text.selection.LocalTextSelectionColors
-import androidx.compose.foundation.text.selection.Selectable
-import androidx.compose.foundation.text.selection.SelectionRegistrar
-import androidx.compose.foundation.text.selection.hasSelection
-import androidx.compose.runtime.Stable
-import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.semantics.getTextLayoutResult
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString
@@ -96,7 +94,6 @@
*/
@Composable
@OptIn(InternalFoundationTextApi::class)
-@Suppress("DEPRECATION") // longPressDragGestureFilter
internal fun CoreText(
text: AnnotatedString,
modifier: Modifier = Modifier,
@@ -170,20 +167,13 @@
.then(
if (selectionRegistrar != null) {
if (isInTouchMode) {
- Modifier.longPressDragGestureFilter(
- longPressDragObserver(
- state = state,
- selectionRegistrar = selectionRegistrar
+ Modifier.pointerInput(controller.longPressDragObserver) {
+ detectDragGesturesAfterLongPressWithObserver(
+ controller.longPressDragObserver
)
- )
+ }
} else {
- Modifier.mouseDragGestureFilter(
- mouseSelectionObserver(
- state = state,
- selectionRegistrar = selectionRegistrar
- ),
- enabled = true
- )
+ Modifier.mouseDragGestureFilter(controller.longPressDragObserver, true)
}
} else {
Modifier
@@ -213,7 +203,8 @@
}
@OptIn(InternalFoundationTextApi::class)
-private class TextController(val state: TextState) {
+/*@VisibleForTesting*/
+internal class TextController(val state: TextState) {
var selectionRegistrar: SelectionRegistrar? = null
fun update(selectionRegistrar: SelectionRegistrar?) {
@@ -227,7 +218,7 @@
if (selectionRegistrar.hasSelection(state.selectableId)) {
val newGlobalPosition = it.positionInWindow()
if (newGlobalPosition != state.previousGlobalPosition) {
- selectionRegistrar?.notifyPositionChange()
+ selectionRegistrar?.notifyPositionChange(state.selectableId)
}
state.previousGlobalPosition = newGlobalPosition
}
@@ -356,6 +347,65 @@
}
}
+ val longPressDragObserver: TextDragObserver = object : TextDragObserver {
+ /**
+ * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
+ * recalculated.
+ */
+ var dragBeginPosition = Offset.Zero
+
+ /**
+ * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
+ * it will be zeroed out.
+ */
+ var dragTotalDistance = Offset.Zero
+
+ override fun onStart(startPoint: Offset) {
+ state.layoutCoordinates?.let {
+ if (!it.isAttached) return
+
+ selectionRegistrar?.notifySelectionUpdateStart(
+ layoutCoordinates = it,
+ startPosition = startPoint
+ )
+
+ dragBeginPosition = startPoint
+ }
+ // selection never started
+ if (!selectionRegistrar.hasSelection(state.selectableId)) return
+ // Zero out the total distance that being dragged.
+ dragTotalDistance = Offset.Zero
+ }
+
+ override fun onDrag(delta: Offset) {
+ state.layoutCoordinates?.let {
+ if (!it.isAttached) return
+ // selection never started, did not consume any drag
+ if (!selectionRegistrar.hasSelection(state.selectableId)) return
+
+ dragTotalDistance += delta
+
+ selectionRegistrar?.notifySelectionUpdate(
+ layoutCoordinates = it,
+ startPosition = dragBeginPosition,
+ endPosition = dragBeginPosition + dragTotalDistance
+ )
+ }
+ }
+
+ override fun onStop() {
+ if (selectionRegistrar.hasSelection(state.selectableId)) {
+ selectionRegistrar?.notifySelectionUpdateEnd()
+ }
+ }
+
+ override fun onCancel() {
+ if (selectionRegistrar.hasSelection(state.selectableId)) {
+ selectionRegistrar?.notifySelectionUpdateEnd()
+ }
+ }
+ }
+
/**
* Draw the given selection on the canvas.
*/
@@ -407,10 +457,13 @@
/** The last layout coordinates for the Text's layout, used by selection */
var layoutCoordinates: LayoutCoordinates? = null
+
/** The latest TextLayoutResult calculated in the measure block */
var layoutResult: TextLayoutResult? = null
+
/** The global position calculated during the last notifyPosition callback */
var previousGlobalPosition: Offset = Offset.Zero
+
/** The paint used to draw highlight background for selected text. */
val selectionPaint: Paint = Paint()
}
@@ -488,115 +541,3 @@
}
return Pair(placeholders, inlineComposables)
}
-
-/*@VisibleForTesting*/
-@Suppress("DEPRECATION") // LongPressDragObserver
-internal fun longPressDragObserver(
- state: TextState,
- selectionRegistrar: SelectionRegistrar?
-): LongPressDragObserver {
- /**
- * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
- * recalculated.
- */
- var dragBeginPosition = Offset.Zero
-
- /**
- * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
- * it will be zeroed out.
- */
- var dragTotalDistance = Offset.Zero
- return object : LongPressDragObserver {
- override fun onLongPress(pxPosition: Offset) {
- state.layoutCoordinates?.let {
- if (!it.isAttached) return
-
- selectionRegistrar?.notifySelectionUpdateStart(
- layoutCoordinates = it,
- startPosition = pxPosition
- )
-
- dragBeginPosition = pxPosition
- }
- }
-
- override fun onDragStart() {
- // selection never started
- if (!selectionRegistrar.hasSelection(state.selectableId)) return
- // Zero out the total distance that being dragged.
- dragTotalDistance = Offset.Zero
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- state.layoutCoordinates?.let {
- if (!it.isAttached) return Offset.Zero
- // selection never started, did not consume any drag
- if (!selectionRegistrar.hasSelection(state.selectableId)) return Offset.Zero
-
- dragTotalDistance += dragDistance
-
- selectionRegistrar?.notifySelectionUpdate(
- layoutCoordinates = it,
- startPosition = dragBeginPosition,
- endPosition = dragBeginPosition + dragTotalDistance
- )
- }
- return dragDistance
- }
-
- override fun onStop(velocity: Offset) {
- if (selectionRegistrar.hasSelection(state.selectableId)) {
- selectionRegistrar?.notifySelectionUpdateEnd()
- }
- }
-
- override fun onCancel() {
- if (selectionRegistrar.hasSelection(state.selectableId)) {
- selectionRegistrar?.notifySelectionUpdateEnd()
- }
- }
- }
-}
-
-@Suppress("DEPRECATION") // DragObserver
-internal fun mouseSelectionObserver(
- state: TextState,
- selectionRegistrar: SelectionRegistrar?
-): DragObserver {
- var dragBeginPosition = Offset.Zero
-
- var dragTotalDistance = Offset.Zero
- return object : DragObserver {
- override fun onStart(downPosition: Offset) {
- state.layoutCoordinates?.let {
- if (!it.isAttached) return
-
- selectionRegistrar?.notifySelectionUpdateStart(
- layoutCoordinates = it,
- startPosition = downPosition
- )
-
- dragBeginPosition = downPosition
- }
-
- if (!selectionRegistrar.hasSelection(state.selectableId)) return
- dragTotalDistance = Offset.Zero
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- state.layoutCoordinates?.let {
- if (!it.isAttached) return Offset.Zero
- if (!selectionRegistrar.hasSelection(state.selectableId)) return Offset.Zero
-
- dragTotalDistance += dragDistance
-
- selectionRegistrar?.notifySelectionUpdate(
- layoutCoordinates = it,
- startPosition = dragBeginPosition,
- endPosition = dragBeginPosition + dragTotalDistance
- )
- }
- return dragDistance
- }
- }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 58f75c4..e86679f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -13,12 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-@file:Suppress("DEPRECATION_ERROR")
package androidx.compose.foundation.text
-import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -269,9 +268,11 @@
if (!it.isFocused) manager.deselect()
}
- val focusRequestTapModifier = Modifier.focusRequestTapModifier(
- enabled = enabled,
- onTap = { offset ->
+ val selectionModifier =
+ Modifier.longPressDragGestureFilter(manager.touchSelectionObserver, enabled)
+
+ val pointerModifier = if (isInTouchMode) {
+ Modifier.tapPressTextFieldModifier(interactionSource, enabled) { offset ->
tapToFocus(state, focusRequester, !readOnly)
if (state.hasFocus) {
if (!state.selectionIsOn) {
@@ -288,16 +289,7 @@
manager.deselect(offset)
}
}
- }
- )
-
- val selectionModifier =
- Modifier.longPressDragGestureFilter(manager.touchSelectionObserver, enabled)
-
- val pointerModifier = if (isInTouchMode) {
- Modifier.pressGestureFilter(interactionSource = interactionSource, enabled = enabled)
- .then(selectionModifier)
- .then(focusRequestTapModifier)
+ }.then(selectionModifier)
} else {
Modifier.mouseDragGestureDetector(manager::mouseSelectionDetector, enabled = enabled)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
index 822bd71..871e49a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardActions.kt
@@ -40,7 +40,7 @@
* indicates that the default implementation should be executed. The default implementation
* moves focus to the next item in the focus traversal order.
*
- * @see [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
+ * See [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
* to specify a custom focus order if needed.
*/
val onNext: (KeyboardActionScope.() -> Unit)? = null,
@@ -50,7 +50,7 @@
* indicates that the default implementation should be executed. The default implementation
* moves focus to the previous item in the focus traversal order.
*
- * @see [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
+ * See [Modifier.focusOrder()][androidx.compose.ui.focus.focusOrder] for more details on how
* to specify a custom focus order if needed.
*/
val onPrevious: (KeyboardActionScope.() -> Unit)? = null,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt
new file mode 100644
index 0000000..81c0edc
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/LongPressTextDragObserver.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.consumeAllChanges
+
+internal interface TextDragObserver {
+ fun onStart(startPoint: Offset)
+
+ fun onDrag(delta: Offset)
+
+ fun onStop()
+
+ fun onCancel()
+}
+
+internal suspend fun PointerInputScope.detectDragGesturesAfterLongPressWithObserver(
+ observer: TextDragObserver
+) = detectDragGesturesAfterLongPress(
+ onDragEnd = { observer.onStop() },
+ onDrag = { change, offset ->
+ change.consumeAllChanges()
+ observer.onDrag(offset)
+ },
+ onDragStart = {
+ observer.onStart(it)
+ },
+ onDragCancel = { observer.onCancel() }
+)
+
+internal suspend fun PointerInputScope.detectDragGesturesWithObserver(
+ observer: TextDragObserver
+) = detectDragGestures(
+ onDragEnd = { observer.onStop() },
+ onDrag = { change, offset ->
+ change.consumeAllChanges()
+ observer.onDrag(offset)
+ },
+ onDragStart = {
+ observer.onStart(it)
+ },
+ onDragCancel = { observer.onCancel() }
+)
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
index b1f6e32..258f2b0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldGestureModifiers.kt
@@ -14,34 +14,33 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION") // gestures
-
package androidx.compose.foundation.text
-import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.focusable
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.FocusState
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.dragGestureFilter
-import androidx.compose.foundation.legacygestures.longPressDragGestureFilter
-import androidx.compose.foundation.legacygestures.tapGestureFilter
import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.consumeAllChanges
+import androidx.compose.ui.input.pointer.consumeDownChange
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
// Touch selection
internal fun Modifier.longPressDragGestureFilter(
- observer: LongPressDragObserver,
+ observer: TextDragObserver,
enabled: Boolean
-) = if (enabled) this.then(longPressDragGestureFilter(observer)) else this
-
-internal fun Modifier.focusRequestTapModifier(onTap: (Offset) -> Unit, enabled: Boolean) =
- if (enabled) this.tapGestureFilter(onTap) else this
+) = if (enabled) {
+ this.pointerInput(observer) { detectDragGesturesAfterLongPressWithObserver(observer) }
+} else {
+ this
+}
// Focus modifiers
internal fun Modifier.textFieldFocusModifier(
@@ -55,10 +54,25 @@
.focusable(interactionSource = interactionSource, enabled = enabled)
// Mouse
-internal fun Modifier.mouseDragGestureFilter(
- dragObserver: DragObserver,
- enabled: Boolean
-) = if (enabled) this.dragGestureFilter(dragObserver, startDragImmediately = true) else this
+internal fun Modifier.mouseDragGestureFilter(dragObserver: TextDragObserver, enabled: Boolean) =
+ if (enabled) {
+ this.pointerInput(dragObserver) {
+ forEachGesture {
+ awaitPointerEventScope {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ down.consumeDownChange()
+ dragObserver.onStart(down.position)
+ drag(down.id) { event ->
+ dragObserver.onDrag(event.positionChange())
+ event.consumeAllChanges()
+ }
+ // specifically don't call observer.onStop/onCancel for mouse case
+ }
+ }
+ }
+ } else {
+ this
+ }
internal fun Modifier.mouseDragGestureDetector(
detector: suspend PointerInputScope.() -> Unit,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
index b792da1..9281ab6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldKeyInput.kt
@@ -18,7 +18,10 @@
import androidx.compose.foundation.text.selection.TextFieldPreparedSelection
import androidx.compose.foundation.text.selection.TextFieldSelectionManager
+import androidx.compose.foundation.text.selection.TextPreparedSelectionState
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.onKeyEvent
@@ -47,6 +50,7 @@
val value: TextFieldValue = TextFieldValue(),
val editable: Boolean = true,
val singleLine: Boolean = false,
+ val preparedSelectionState: TextPreparedSelectionState,
val offsetMapping: OffsetMapping = OffsetMapping.Identity,
private val keyMapping: KeyMapping = platformDefaultKeyMapping,
) {
@@ -67,6 +71,7 @@
typedCommand(event)?.let {
return if (editable) {
it.apply()
+ preparedSelectionState.resetCachedX()
true
} else {
false
@@ -165,7 +170,8 @@
val preparedSelection = TextFieldPreparedSelection(
currentValue = value,
offsetMapping = offsetMapping,
- layoutResultProxy = state.layoutResult
+ layoutResultProxy = state.layoutResult,
+ state = preparedSelectionState
)
block(preparedSelection)
if (preparedSelection.selection != value.selection ||
@@ -176,6 +182,7 @@
}
}
+@Suppress("ModifierInspectorInfo")
internal fun Modifier.textFieldKeyInput(
state: TextFieldState,
manager: TextFieldSelectionManager,
@@ -183,14 +190,16 @@
editable: Boolean,
singleLine: Boolean,
offsetMapping: OffsetMapping
-): Modifier {
+) = composed {
+ val preparedSelectionState = remember { TextPreparedSelectionState() }
val processor = TextFieldKeyInput(
state = state,
selectionManager = manager,
value = value,
editable = editable,
singleLine = singleLine,
- offsetMapping = offsetMapping
+ offsetMapping = offsetMapping,
+ preparedSelectionState = preparedSelectionState
)
- return Modifier.onKeyEvent(processor::process)
+ Modifier.onKeyEvent(processor::process)
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
index 493e81c..24af865 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldPressGestureFilter.kt
@@ -16,27 +16,32 @@
package androidx.compose.foundation.text
+import androidx.compose.foundation.gestures.detectTapAndPress
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.pointerInput
import kotlinx.coroutines.launch
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
/**
- * Required for the press [MutableInteractionSource] consistency for TextField.
+ * Required for the press and tap [MutableInteractionSource] consistency for TextField.
*/
-@Suppress("ModifierInspectorInfo", "DEPRECATION")
-internal fun Modifier.pressGestureFilter(
+@Suppress("ModifierInspectorInfo")
+internal fun Modifier.tapPressTextFieldModifier(
interactionSource: MutableInteractionSource?,
- enabled: Boolean = true
+ enabled: Boolean = true,
+ onTap: (Offset) -> Unit
): Modifier = if (enabled) composed {
val scope = rememberCoroutineScope()
val pressedInteraction = remember { mutableStateOf<PressInteraction.Press?>(null) }
+ val onTapState = rememberUpdatedState(onTap)
DisposableEffect(interactionSource) {
onDispose {
pressedInteraction.value?.let { oldValue ->
@@ -46,37 +51,35 @@
}
}
}
- pressIndicatorGestureFilter(
- onStart = {
- scope.launch {
- // Remove any old interactions if we didn't fire stop / cancel properly
- pressedInteraction.value?.let { oldValue ->
- val interaction = PressInteraction.Cancel(oldValue)
+ Modifier.pointerInput(interactionSource) {
+ detectTapAndPress(
+ onPress = {
+ scope.launch {
+ // Remove any old interactions if we didn't fire stop / cancel properly
+ pressedInteraction.value?.let { oldValue ->
+ val interaction = PressInteraction.Cancel(oldValue)
+ interactionSource?.emit(interaction)
+ pressedInteraction.value = null
+ }
+ val interaction = PressInteraction.Press(it)
interactionSource?.emit(interaction)
- pressedInteraction.value = null
+ pressedInteraction.value = interaction
}
- val interaction = PressInteraction.Press(it)
- interactionSource?.emit(interaction)
- pressedInteraction.value = interaction
- }
- },
- onStop = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Release(it)
- interactionSource?.emit(interaction)
- pressedInteraction.value = null
+ val success = tryAwaitRelease()
+ scope.launch {
+ pressedInteraction.value?.let { oldValue ->
+ val interaction =
+ if (success) {
+ PressInteraction.Release(oldValue)
+ } else {
+ PressInteraction.Cancel(oldValue)
+ }
+ interactionSource?.emit(interaction)
+ pressedInteraction.value = null
+ }
}
- }
- },
- onCancel = {
- scope.launch {
- pressedInteraction.value?.let {
- val interaction = PressInteraction.Cancel(it)
- interactionSource?.emit(interaction)
- pressedInteraction.value = null
- }
- }
- }
- )
+ },
+ onTap = { onTapState.value.invoke(it) }
+ )
+ }
} else this
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 413118a..d3da4b5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.text.selection
+import androidx.compose.foundation.text.detectDragGesturesWithObserver
import androidx.compose.foundation.text.isInTouchMode
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -26,7 +27,7 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.foundation.legacygestures.dragGestureFilter
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalTextToolbar
@@ -71,7 +72,7 @@
* area with start and end handles.
*/
@OptIn(ExperimentalComposeUiApi::class)
-@Suppress("ComposableLambdaParameterNaming", "DEPRECATION")
+@Suppress("ComposableLambdaParameterNaming")
@Composable
internal fun SelectionContainer(
/** A [Modifier] for SelectionContainer. */
@@ -100,17 +101,18 @@
if (isInTouchMode && manager.hasFocus) {
manager.selection?.let {
listOf(true, false).fastForEach { isStartHandle ->
+ val observer = remember(isStartHandle) {
+ manager.handleDragObserver(isStartHandle)
+ }
SelectionHandle(
startHandlePosition = manager.startHandlePosition,
endHandlePosition = manager.endHandlePosition,
isStartHandle = isStartHandle,
directions = Pair(it.start.direction, it.end.direction),
handlesCrossed = it.handlesCrossed,
- modifier = Modifier.dragGestureFilter(
- manager.handleDragObserver(
- isStartHandle
- )
- ),
+ modifier = Modifier.pointerInput(observer) {
+ detectDragGesturesWithObserver(observer)
+ },
handle = null
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 53acc0b..cdbf21c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -32,7 +32,7 @@
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.foundation.legacygestures.DragObserver
+import androidx.compose.foundation.text.TextDragObserver
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.key.KeyEvent
@@ -40,6 +40,7 @@
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
@@ -121,13 +122,16 @@
}
}
+ private var previousPosition: Offset? = null
/**
* Layout Coordinates of the selection container.
*/
var containerLayoutCoordinates: LayoutCoordinates? = null
set(value) {
field = value
- if (hasFocus) {
+ val positionInWindow = value?.positionInWindow()
+ if (hasFocus && previousPosition != positionInWindow) {
+ previousPosition = positionInWindow
updateHandleOffsets()
updateSelectionToolbarPosition()
}
@@ -170,9 +174,14 @@
private set
init {
- selectionRegistrar.onPositionChangeCallback = {
- updateHandleOffsets()
- updateSelectionToolbarPosition()
+ selectionRegistrar.onPositionChangeCallback = { selectableId ->
+ if (
+ selectableId == selection?.start?.selectableId ||
+ selectableId == selection?.end?.selectableId
+ ) {
+ updateHandleOffsets()
+ updateSelectionToolbarPosition()
+ }
}
selectionRegistrar.onSelectionUpdateStartCallback = { layoutCoordinates, startPosition ->
@@ -458,9 +467,9 @@
}
}
- fun handleDragObserver(isStartHandle: Boolean): DragObserver {
- return object : DragObserver {
- override fun onStart(downPosition: Offset) {
+ fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
+ return object : TextDragObserver {
+ override fun onStart(startPoint: Offset) {
hideSelectionToolbar()
val selection = selection!!
val startSelectable =
@@ -501,9 +510,9 @@
dragTotalDistance = Offset.Zero
}
- override fun onDrag(dragDistance: Offset): Offset {
+ override fun onDrag(delta: Offset) {
val selection = selection!!
- dragTotalDistance += dragDistance
+ dragTotalDistance += delta
val startSelectable =
selectionRegistrar.selectableMap[selection.start.selectableId]
val endSelectable =
@@ -540,10 +549,10 @@
endPosition = currentEnd,
isStartHandle = isStartHandle
)
- return dragDistance
+ return
}
- override fun onStop(velocity: Offset) {
+ override fun onStop() {
showSelectionToolbar()
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
index 5d6fdab..25ae682 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrar.kt
@@ -55,7 +55,7 @@
* When the Global Position of a subscribed [Selectable] changes, this method
* is called.
*/
- fun notifyPositionChange()
+ fun notifyPositionChange(selectableId: Long)
/**
* Call this method to notify the [SelectionContainer] that the selection has been initiated.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
index 3cda909..f43c89f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImpl.kt
@@ -59,7 +59,7 @@
/**
* The callback to be invoked when the position change was triggered.
*/
- internal var onPositionChangeCallback: (() -> Unit)? = null
+ internal var onPositionChangeCallback: ((Long) -> Unit)? = null
/**
* The callback to be invoked when the selection is initiated.
@@ -150,11 +150,11 @@
return selectables
}
- override fun notifyPositionChange() {
+ override fun notifyPositionChange(selectableId: Long) {
// Set the variable sorted to be false, when the global position of a registered
// selectable changes.
sorted = false
- onPositionChangeCallback?.invoke()
+ onPositionChangeCallback?.invoke(selectableId)
}
override fun notifySelectionUpdateStart(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 55616a6..b36438c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -14,26 +14,24 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION") // gestures
-
package androidx.compose.foundation.text.selection
import androidx.compose.foundation.gestures.drag
import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.text.InternalFoundationTextApi
+import androidx.compose.foundation.text.TextDragObserver
import androidx.compose.foundation.text.TextFieldDelegate
import androidx.compose.foundation.text.TextFieldState
+import androidx.compose.foundation.text.detectDragGesturesWithObserver
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.foundation.legacygestures.dragGestureFilter
-import androidx.compose.foundation.text.InternalFoundationTextApi
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.AwaitPointerEventScope
@@ -43,6 +41,7 @@
import androidx.compose.ui.input.pointer.changedToDown
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.consumeDownChange
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.TextToolbar
@@ -146,20 +145,20 @@
private var oldValue: TextFieldValue = TextFieldValue()
/**
- * [LongPressDragObserver] for long press and drag to select in TextField.
+ * [TextDragObserver] for long press and drag to select in TextField.
*/
- internal val touchSelectionObserver = object : LongPressDragObserver {
- override fun onLongPress(pxPosition: Offset) {
+ internal val touchSelectionObserver = object : TextDragObserver {
+ override fun onStart(startPoint: Offset) {
state?.let {
if (it.draggingHandle) return
}
// Long Press at the blank area, the cursor should show up at the end of the line.
- if (state?.layoutResult?.isPositionOnText(pxPosition) != true) {
+ if (state?.layoutResult?.isPositionOnText(startPoint) != true) {
state?.layoutResult?.let { layoutResult ->
val offset = offsetMapping.transformedToOriginal(
layoutResult.getLineEnd(
- layoutResult.getLineForVerticalPosition(pxPosition.y)
+ layoutResult.getLineForVerticalPosition(startPoint.y)
)
)
hapticFeedBack?.performHapticFeedback(HapticFeedbackType.TextHandleMove)
@@ -178,7 +177,7 @@
if (value.text.isEmpty()) return
enterSelectionMode()
state?.layoutResult?.let { layoutResult ->
- val offset = layoutResult.getOffsetForPosition(pxPosition)
+ val offset = layoutResult.getOffsetForPosition(startPoint)
updateSelection(
value = value,
transformedStartOffset = offset,
@@ -188,15 +187,15 @@
)
dragBeginOffsetInText = offset
}
- dragBeginPosition = pxPosition
+ dragBeginPosition = startPoint
dragTotalDistance = Offset.Zero
}
- override fun onDrag(dragDistance: Offset): Offset {
+ override fun onDrag(delta: Offset) {
// selection never started, did not consume any drag
- if (value.text.isEmpty()) return Offset.Zero
+ if (value.text.isEmpty()) return
- dragTotalDistance += dragDistance
+ dragTotalDistance += delta
state?.layoutResult?.let { layoutResult ->
val startOffset = dragBeginOffsetInText ?: layoutResult.getOffsetForPosition(
position = dragBeginPosition,
@@ -215,15 +214,15 @@
)
}
state?.showFloatingToolbar = false
- return dragDistance
}
- override fun onStop(velocity: Offset) {
- super.onStop(velocity)
+ override fun onStop() {
state?.showFloatingToolbar = true
if (textToolbar?.status == TextToolbarStatus.Hidden) showSelectionToolbar()
dragBeginOffsetInText = null
}
+
+ override fun onCancel() {}
}
internal interface MouseSelectionObserver {
@@ -267,6 +266,7 @@
}
}
}
+
override fun onDrag(dragDistance: Offset) {
if (value.text.isEmpty()) return
@@ -305,11 +305,11 @@
}
/**
- * [DragObserver] for dragging the selection handles to change the selection in TextField.
+ * [TextDragObserver] for dragging the selection handles to change the selection in TextField.
*/
- internal fun handleDragObserver(isStartHandle: Boolean): DragObserver {
- return object : DragObserver {
- override fun onStart(downPosition: Offset) {
+ internal fun handleDragObserver(isStartHandle: Boolean): TextDragObserver {
+ return object : TextDragObserver {
+ override fun onStart(startPoint: Offset) {
// The position of the character where the drag gesture should begin. This is in
// the composable coordinates.
dragBeginPosition = getAdjustedCoordinates(getHandlePosition(isStartHandle))
@@ -319,8 +319,8 @@
state?.showFloatingToolbar = false
}
- override fun onDrag(dragDistance: Offset): Offset {
- dragTotalDistance += dragDistance
+ override fun onDrag(delta: Offset) {
+ dragTotalDistance += delta
state?.layoutResult?.value?.let { layoutResult ->
val startOffset = if (isStartHandle)
@@ -342,15 +342,15 @@
)
}
state?.showFloatingToolbar = false
- return dragDistance
}
- override fun onStop(velocity: Offset) {
- super.onStop(velocity)
+ override fun onStop() {
state?.draggingHandle = false
state?.showFloatingToolbar = true
if (textToolbar?.status == TextToolbarStatus.Hidden) showSelectionToolbar()
}
+
+ override fun onCancel() {}
}
}
@@ -670,13 +670,18 @@
directions: Pair<ResolvedTextDirection, ResolvedTextDirection>,
manager: TextFieldSelectionManager
) {
+ val observer = remember(isStartHandle, manager) {
+ manager.handleDragObserver(isStartHandle)
+ }
SelectionHandle(
startHandlePosition = manager.getHandlePosition(true),
endHandlePosition = manager.getHandlePosition(false),
isStartHandle = isStartHandle,
directions = directions,
handlesCrossed = manager.value.selection.reversed,
- modifier = Modifier.dragGestureFilter(manager.handleDragObserver(isStartHandle)),
+ modifier = Modifier.pointerInput(observer) {
+ detectDragGesturesWithObserver(observer)
+ },
handle = null
)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
index 68d1712..cb83d7b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextPreparedSelection.kt
@@ -30,6 +30,16 @@
import kotlin.math.max
import kotlin.math.min
+internal class TextPreparedSelectionState {
+ // it's set at the start of vertical navigation and used as the preferred value to set a new
+ // cursor position.
+ var cachedX: Float? = null
+
+ fun resetCachedX() {
+ cachedX = null
+ }
+}
+
/**
* This utility class implements many selection-related operations on text (including basic
* cursor movements and deletions) and combines them, taking into account how the text was
@@ -47,25 +57,29 @@
val originalText: AnnotatedString,
val originalSelection: TextRange,
val layoutResult: TextLayoutResult?,
- val offsetMapping: OffsetMapping
+ val offsetMapping: OffsetMapping,
+ val state: TextPreparedSelectionState
) {
var selection = originalSelection
var annotatedString = originalText
- protected val text
+ internal val text
get() = annotatedString.text
@Suppress("UNCHECKED_CAST")
- inline fun <U> U.apply(block: U.() -> Unit): T {
+ protected inline fun <U> U.apply(resetCachedX: Boolean = true, block: U.() -> Unit): T {
+ if (resetCachedX) {
+ state.resetCachedX()
+ }
block()
return this as T
}
- fun setCursor(offset: Int) = apply {
+ protected fun setCursor(offset: Int) {
setSelection(offset, offset)
}
- fun setSelection(start: Int, end: Int) = apply {
+ protected fun setSelection(start: Int, end: Int) {
selection = TextRange(start, end)
}
@@ -175,11 +189,11 @@
setCursor(getParagraphEnd())
}
- fun moveCursorUpByLine() = apply {
+ fun moveCursorUpByLine() = apply(false) {
layoutResult?.jumpByLinesOffset(-1)?.let { setCursor(it) }
}
- fun moveCursorDownByLine() = apply {
+ fun moveCursorDownByLine() = apply(false) {
layoutResult?.jumpByLinesOffset(1)?.let { setCursor(it) }
}
@@ -208,11 +222,16 @@
}
// it selects a text from the original selection start to a current selection end
- fun selectMovement() = apply {
+ fun selectMovement() = apply(false) {
selection = TextRange(originalSelection.start, selection.end)
}
- // delete currently selected text and update [selection] and [annotatedString]
+ /**
+ * delete currently selected text and update [selection] and [annotatedString]
+ *
+ * it supposed to be the last operation, it doesn't relayout text by itself, so any
+ * subsequent calls could give wrong results
+ */
fun deleteSelected() = apply {
val maxChars = text.length
val beforeSelection =
@@ -224,7 +243,7 @@
}
private fun isLtr(): Boolean {
- val direction = layoutResult?.getBidiRunDirection(selection.end)
+ val direction = layoutResult?.getParagraphDirection(selection.end)
return direction != ResolvedTextDirection.Rtl
}
@@ -273,22 +292,26 @@
private fun TextLayoutResult.jumpByLinesOffset(linesAmount: Int): Int {
val currentOffset = transformedEndOffset()
- val newLine = getLineForOffset(currentOffset) + linesAmount
+ if (state.cachedX == null) {
+ state.cachedX = getCursorRect(currentOffset).left
+ }
+
+ val targetLine = getLineForOffset(currentOffset) + linesAmount
when {
- newLine < 0 -> {
+ targetLine < 0 -> {
return 0
}
- newLine >= lineCount -> {
+ targetLine >= lineCount -> {
return text.length
}
}
- val y = getLineBottom(newLine) - 1
- val x = getCursorRect(currentOffset).left.also {
- if ((isLtr() && it >= getLineRight(newLine)) ||
- (!isLtr() && it <= getLineLeft(newLine))
+ val y = getLineBottom(targetLine) - 1
+ val x = state.cachedX!!.also {
+ if ((isLtr() && it >= getLineRight(targetLine)) ||
+ (!isLtr() && it <= getLineLeft(targetLine))
) {
- return getLineEnd(newLine, true)
+ return getLineEnd(targetLine, true)
}
}
@@ -300,19 +323,19 @@
}
private fun transformedEndOffset(): Int {
- return offsetMapping.originalToTransformed(originalSelection.end)
+ return offsetMapping.originalToTransformed(selection.end)
}
private fun transformedMinOffset(): Int {
- return offsetMapping.originalToTransformed(originalSelection.min)
+ return offsetMapping.originalToTransformed(selection.min)
}
private fun transformedMaxOffset(): Int {
- return offsetMapping.originalToTransformed(originalSelection.max)
+ return offsetMapping.originalToTransformed(selection.max)
}
private fun charOffset(offset: Int) =
- offset.coerceAtMost(originalText.length - 1)
+ offset.coerceAtMost(text.length - 1)
private fun getParagraphStart(): Int {
var index = selection.min
@@ -343,15 +366,31 @@
}
}
+internal class TextPreparedSelection(
+ originalText: AnnotatedString,
+ originalSelection: TextRange,
+ layoutResult: TextLayoutResult? = null,
+ offsetMapping: OffsetMapping = OffsetMapping.Identity,
+ state: TextPreparedSelectionState = TextPreparedSelectionState()
+) : BaseTextPreparedSelection<TextPreparedSelection>(
+ originalText = originalText,
+ originalSelection = originalSelection,
+ layoutResult = layoutResult,
+ offsetMapping = offsetMapping,
+ state = state
+)
+
internal class TextFieldPreparedSelection(
val currentValue: TextFieldValue,
offsetMapping: OffsetMapping = OffsetMapping.Identity,
- val layoutResultProxy: TextLayoutResultProxy?
+ val layoutResultProxy: TextLayoutResultProxy?,
+ state: TextPreparedSelectionState = TextPreparedSelectionState()
) : BaseTextPreparedSelection<TextFieldPreparedSelection>(
originalText = currentValue.annotatedString,
originalSelection = currentValue.selection,
offsetMapping = offsetMapping,
- layoutResult = layoutResultProxy?.value
+ layoutResult = layoutResultProxy?.value,
+ state = state
) {
val value
get() = currentValue.copy(
@@ -367,11 +406,11 @@
}
}
- fun moveCursorUpByPage() = apply {
+ fun moveCursorUpByPage() = apply(false) {
layoutResultProxy?.jumpByPagesOffset(-1)?.let { setCursor(it) }
}
- fun moveCursorDownByPage() = apply {
+ fun moveCursorDownByPage() = apply(false) {
layoutResultProxy?.jumpByPagesOffset(1)?.let { setCursor(it) }
}
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
index eab10ec..71ed883 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt
@@ -14,36 +14,39 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION")
-
package androidx.compose.foundation
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.DragObserver
-import androidx.compose.foundation.legacygestures.pressIndicatorGestureFilter
-import androidx.compose.foundation.legacygestures.rawDragGestureFilter
import androidx.compose.foundation.interaction.DragInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.gestures.detectTapAndPress
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerMoveFilter
+import androidx.compose.ui.input.pointer.consumePositionChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.platform.LocalDensity
@@ -53,7 +56,6 @@
import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt
import kotlin.math.sign
@@ -181,7 +183,6 @@
interactionSource: MutableInteractionSource,
isVertical: Boolean
) = with(LocalDensity.current) {
- val scope = rememberCoroutineScope()
val dragInteraction = remember { mutableStateOf<DragInteraction.Start?>(null) }
DisposableEffect(interactionSource) {
onDispose {
@@ -211,48 +212,6 @@
}
}
- val dragObserver = object : DragObserver {
- override fun onStart(downPosition: Offset) {
- scope.launch {
- dragInteraction.value?.let { oldInteraction ->
- interactionSource.emit(
- DragInteraction.Cancel(oldInteraction)
- )
- }
- val interaction = DragInteraction.Start()
- interactionSource.emit(interaction)
- dragInteraction.value = interaction
- }
- }
-
- override fun onStop(velocity: Offset) {
- scope.launch {
- dragInteraction.value?.let { interaction ->
- interactionSource.emit(
- DragInteraction.Stop(interaction)
- )
- dragInteraction.value = null
- }
- }
- }
-
- override fun onCancel() {
- scope.launch {
- dragInteraction.value?.let { interaction ->
- interactionSource.emit(
- DragInteraction.Cancel(interaction)
- )
- dragInteraction.value = null
- }
- }
- }
-
- override fun onDrag(dragDistance: Offset): Offset {
- sliderAdapter.position += if (isVertical) dragDistance.y else dragDistance.x
- return dragDistance
- }
- }
-
val color by animateColorAsState(
if (isHover) style.hoverColor else style.unhoverColor,
animationSpec = TweenSpec(durationMillis = style.hoverDurationMillis)
@@ -265,7 +224,9 @@
Box(
Modifier
.background(if (isVisible) color else Color.Transparent, style.shape)
- .rawDragGestureFilter(dragObserver)
+ .scrollbarDrag(interactionSource, dragInteraction) { offset ->
+ sliderAdapter.position += if (isVertical) offset.y else offset.x
+ }
)
},
modifier
@@ -278,7 +239,32 @@
)
}
-@Suppress("DEPRECATION") // press gesture filter
+private fun Modifier.scrollbarDrag(
+ interactionSource: MutableInteractionSource,
+ draggedInteraction: MutableState<DragInteraction.Start?>,
+ onDelta: (Offset) -> Unit
+): Modifier = pointerInput(interactionSource, draggedInteraction, onDelta) {
+ forEachGesture {
+ awaitPointerEventScope {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ val interaction = DragInteraction.Start()
+ interactionSource.tryEmit(interaction)
+ draggedInteraction.value = interaction
+ val isSuccess = drag(down.id) { change ->
+ onDelta.invoke(change.positionChange())
+ change.consumePositionChange()
+ }
+ val finishInteraction = if (isSuccess) {
+ DragInteraction.Stop(interaction)
+ } else {
+ DragInteraction.Cancel(interaction)
+ }
+ interactionSource.tryEmit(finishInteraction)
+ draggedInteraction.value = null
+ }
+ }
+}
+
private fun Modifier.scrollOnPressOutsideSlider(
isVertical: Boolean,
sliderAdapter: SliderAdapter,
@@ -309,12 +295,16 @@
}
}
}
-
- pressIndicatorGestureFilter(
- onStart = { targetOffset = it },
- onStop = { targetOffset = null },
- onCancel = { targetOffset = null }
- )
+ Modifier.pointerInput(Unit) {
+ detectTapAndPress(
+ onPress = { offset ->
+ targetOffset = offset
+ tryAwaitRelease()
+ targetOffset = null
+ },
+ onTap = {}
+ )
+ }
}
/**
@@ -546,4 +536,4 @@
* The time that must elapse before a tap gesture sends onTapDown, if there's
* any doubt that the gesture is a tap.
*/
-private const val PressTimeoutMillis: Long = 100L
\ No newline at end of file
+private const val PressTimeoutMillis: Long = 100L
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
index 1e62c8e..b5ade04 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/MutatorMutexTest.kt
@@ -18,7 +18,6 @@
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -32,7 +31,6 @@
import org.junit.runners.JUnit4
@Suppress("RemoveExplicitTypeArguments")
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(JUnit4::class)
class MutatorMutexTest {
interface MutateCaller {
@@ -42,12 +40,12 @@
): R
}
- class MutateWithoutReceiverCaller(val mutex: MutatorMutex) : MutateCaller {
+ class MutateWithoutReceiverCaller(private val mutex: MutatorMutex) : MutateCaller {
override suspend fun <R> mutate(priority: MutatePriority, block: suspend () -> R): R =
mutex.mutate(priority, block)
}
- class MutateWithReceiverCaller(val mutex: MutatorMutex) : MutateCaller {
+ class MutateWithReceiverCaller(private val mutex: MutatorMutex) : MutateCaller {
override suspend fun <R> mutate(priority: MutatePriority, block: suspend () -> R): R {
val receiver = Any()
return mutex.mutateWith(receiver, priority) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
index 8f068f1..058acdd 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/TapGestureDetectorTest.kt
@@ -62,6 +62,22 @@
)
}
+ private val utilWithShortcut = SuspendingGestureTestUtil {
+ detectTapAndPress(
+ onPress = {
+ pressed = true
+ if (tryAwaitRelease()) {
+ released = true
+ } else {
+ canceled = true
+ }
+ },
+ onTap = {
+ tapped = true
+ }
+ )
+ }
+
private val allGestures = SuspendingGestureTestUtil {
detectTapGestures(
onPress = {
@@ -113,6 +129,26 @@
* Clicking in the region should result in the callback being invoked.
*/
@Test
+ fun normalTap_withShortcut() = utilWithShortcut.executeInComposition {
+ val down = down(5f, 5f)
+ assertTrue(down.consumed.downChange)
+
+ assertTrue(pressed)
+ assertFalse(tapped)
+ assertFalse(released)
+
+ val up = down.up(50)
+ assertTrue(up.consumed.downChange)
+
+ assertTrue(tapped)
+ assertTrue(released)
+ assertFalse(canceled)
+ }
+
+ /**
+ * Clicking in the region should result in the callback being invoked.
+ */
+ @Test
fun normalTapWithAllGestures() = allGestures.executeInComposition {
val down = down(5f, 5f)
assertTrue(down.consumed.downChange)
@@ -206,6 +242,23 @@
* the callback not being invoked
*/
@Test
+ fun tapMiss_withShortcut() = utilWithShortcut.executeInComposition {
+ val up = down(5f, 5f)
+ .moveTo(15f, 15f)
+ .up()
+
+ assertTrue(pressed)
+ assertTrue(canceled)
+ assertFalse(released)
+ assertFalse(tapped)
+ assertFalse(up.consumed.downChange)
+ }
+
+ /**
+ * Pressing in the region, sliding out and then lifting should result in
+ * the callback not being invoked
+ */
+ @Test
fun longPressMiss() = allGestures.executeInComposition {
val pointer = down(5f, 5f)
.moveTo(15f, 15f)
@@ -271,6 +324,24 @@
}
/**
+ * Pressing in the region, sliding out, then back in, then lifting
+ * should result the gesture being canceled.
+ */
+ @Test
+ fun tapOutAndIn_withShortcut() = utilWithShortcut.executeInComposition {
+ val up = down(5f, 5f)
+ .moveTo(15f, 15f)
+ .moveTo(6f, 6f)
+ .up()
+
+ assertFalse(tapped)
+ assertFalse(up.consumed.downChange)
+ assertTrue(pressed)
+ assertFalse(released)
+ assertTrue(canceled)
+ }
+
+ /**
* After a first tap, a second tap should also be detected.
*/
@Test
@@ -296,6 +367,31 @@
}
/**
+ * After a first tap, a second tap should also be detected.
+ */
+ @Test
+ fun secondTap_withShortcut() = utilWithShortcut.executeInComposition {
+ down(5f, 5f)
+ .up()
+
+ assertTrue(pressed)
+ assertTrue(released)
+ assertFalse(canceled)
+
+ tapped = false
+ pressed = false
+ released = false
+
+ val up2 = down(4f, 4f)
+ .up()
+ assertTrue(tapped)
+ assertTrue(up2.consumed.downChange)
+ assertTrue(pressed)
+ assertTrue(released)
+ assertFalse(canceled)
+ }
+
+ /**
* Clicking in the region with the up already consumed should result in the callback not
* being invoked.
*/
@@ -316,6 +412,26 @@
}
/**
+ * Clicking in the region with the up already consumed should result in the callback not
+ * being invoked.
+ */
+ @Test
+ fun consumedUpTap_withShortcut() = utilWithShortcut.executeInComposition {
+ val down = down(5f, 5f)
+
+ assertFalse(tapped)
+ assertTrue(pressed)
+
+ down.up {
+ consumeDownChange()
+ }
+
+ assertFalse(tapped)
+ assertFalse(released)
+ assertTrue(canceled)
+ }
+
+ /**
* Clicking in the region with the motion consumed should result in the callback not
* being invoked.
*/
@@ -334,6 +450,24 @@
}
/**
+ * Clicking in the region with the motion consumed should result in the callback not
+ * being invoked.
+ */
+ @Test
+ fun consumedMotionTap_withShortcut() = utilWithShortcut.executeInComposition {
+ down(5f, 5f)
+ .moveTo(6f, 2f) {
+ consumePositionChange()
+ }
+ .up(50)
+
+ assertFalse(tapped)
+ assertTrue(pressed)
+ assertFalse(released)
+ assertTrue(canceled)
+ }
+
+ /**
* Ensure that two-finger taps work.
*/
@Test
@@ -363,6 +497,35 @@
}
/**
+ * Ensure that two-finger taps work.
+ */
+ @Test
+ fun twoFingerTap_withShortcut() = utilWithShortcut.executeInComposition {
+ val down = down(1f, 1f)
+ assertTrue(down.consumed.downChange)
+
+ assertTrue(pressed)
+ pressed = false
+
+ val down2 = down(9f, 5f)
+ assertFalse(down2.consumed.downChange)
+
+ assertFalse(pressed)
+
+ val up = down.up()
+ assertFalse(up.consumed.downChange)
+ assertFalse(tapped)
+ assertFalse(released)
+
+ val up2 = down2.up()
+ assertTrue(up2.consumed.downChange)
+
+ assertTrue(tapped)
+ assertTrue(released)
+ assertFalse(canceled)
+ }
+
+ /**
* A position change consumption on any finger should cause tap to cancel.
*/
@Test
@@ -389,6 +552,32 @@
}
/**
+ * A position change consumption on any finger should cause tap to cancel.
+ */
+ @Test
+ fun twoFingerTapCancel_withShortcut() = utilWithShortcut.executeInComposition {
+ val down = down(1f, 1f)
+
+ assertTrue(pressed)
+
+ val down2 = down(9f, 5f)
+
+ val up = down.moveTo(5f, 5f) {
+ consumePositionChange()
+ }.up()
+ assertFalse(up.consumed.downChange)
+
+ assertFalse(tapped)
+ assertTrue(canceled)
+
+ val up2 = down2.up(50)
+ assertFalse(up2.consumed.downChange)
+
+ assertFalse(tapped)
+ assertFalse(released)
+ }
+
+ /**
* Detect the second tap as long press.
*/
@Test
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 8fee380..a1583ec 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -14,18 +14,14 @@
* limitations under the License.
*/
-@file:Suppress("DEPRECATION") // LongPressDragObserver
-
package androidx.compose.foundation.text
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.foundation.legacygestures.LongPressDragObserver
-import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.foundation.text.selection.Selectable
import androidx.compose.foundation.text.selection.Selection
import androidx.compose.foundation.text.selection.SelectionRegistrarImpl
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.style.ResolvedTextDirection
-import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
@@ -59,7 +55,7 @@
)
)
- private lateinit var gesture: LongPressDragObserver
+ private lateinit var gesture: TextDragObserver
private lateinit var layoutCoordinates: LayoutCoordinates
private lateinit var state: TextState
@@ -74,17 +70,17 @@
state = TextState(mock(), selectableId)
state.layoutCoordinates = layoutCoordinates
- gesture = longPressDragObserver(
- state = state,
- selectionRegistrar = selectionRegistrar
- )
+ val controller = TextController(state).also {
+ it.update(selectionRegistrar)
+ }
+ gesture = controller.longPressDragObserver
}
@Test
fun longPressDragObserver_onLongPress_calls_notifySelectionInitiated() {
val position = Offset(100f, 100f)
- gesture.onLongPress(position)
+ gesture.onStart(position)
verify(selectionRegistrar, times(1)).notifySelectionUpdateStart(
layoutCoordinates = layoutCoordinates,
@@ -99,17 +95,15 @@
val beginPosition1 = Offset(30f, 20f)
val dragDistance2 = Offset(100f, 300f)
val beginPosition2 = Offset(300f, 200f)
- gesture.onLongPress(beginPosition1)
- gesture.onDragStart()
+ gesture.onStart(beginPosition1)
gesture.onDrag(dragDistance1)
// Setup. Cancel selection and reselect.
// selectionManager.onRelease()
// Start the new selection
- gesture.onLongPress(beginPosition2)
+ gesture.onStart(beginPosition2)
selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
// Act. Reset selectionManager.dragTotalDistance to zero.
- gesture.onDragStart()
gesture.onDrag(dragDistance2)
// Verify.
@@ -125,13 +119,10 @@
fun longPressDragObserver_onDrag_calls_notifySelectionDrag() {
val dragDistance = Offset(15f, 10f)
val beginPosition = Offset(30f, 20f)
- gesture.onLongPress(beginPosition)
+ gesture.onStart(beginPosition)
selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
- gesture.onDragStart()
- val result = gesture.onDrag(dragDistance)
-
- assertThat(result).isEqualTo(dragDistance)
+ gesture.onDrag(dragDistance)
verify(selectionRegistrar, times(1))
.notifySelectionUpdate(
layoutCoordinates = layoutCoordinates,
@@ -142,12 +133,10 @@
@Test
fun longPressDragObserver_onStop_calls_notifySelectionEnd() {
- val dragDistance = Offset(15f, 10f)
val beginPosition = Offset(30f, 20f)
- gesture.onLongPress(beginPosition)
+ gesture.onStart(beginPosition)
selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
- gesture.onDragStart()
- gesture.onStop(dragDistance)
+ gesture.onStop()
verify(selectionRegistrar, times(1))
.notifySelectionUpdateEnd()
@@ -156,9 +145,8 @@
@Test
fun longPressDragObserver_onCancel_calls_notifySelectionEnd() {
val beginPosition = Offset(30f, 20f)
- gesture.onLongPress(beginPosition)
+ gesture.onStart(beginPosition)
selectionRegistrar.subselections = mapOf(selectableId to fakeSelection)
- gesture.onDragStart()
gesture.onCancel()
verify(selectionRegistrar, times(1))
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
index e89b190..07ae5d7 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionManagerDragTest.kt
@@ -144,7 +144,7 @@
val dragDistance = Offset(100f, 100f)
selectionManager.handleDragObserver(isStartHandle = true).onStart(startOffset)
- val result = selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
+ selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
verify(containerLayoutCoordinates, times(1))
.localPositionOf(
@@ -163,7 +163,6 @@
assertThat(selection).isEqualTo(fakeResultSelection)
verify(spyLambda, times(1)).invoke(fakeResultSelection)
- assertThat(result).isEqualTo(dragDistance)
}
@Test
@@ -172,7 +171,7 @@
val dragDistance = Offset(100f, 100f)
selectionManager.handleDragObserver(isStartHandle = false).onStart(startOffset)
- val result = selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
+ selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
verify(containerLayoutCoordinates, times(1))
.localPositionOf(
@@ -191,7 +190,6 @@
assertThat(selection).isEqualTo(fakeResultSelection)
verify(spyLambda, times(1)).invoke(fakeResultSelection)
- assertThat(result).isEqualTo(dragDistance)
}
private fun getAdjustedCoordinates(position: Offset): Offset {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
index d5e37c6..77af370 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/SelectionRegistrarImplTest.kt
@@ -212,7 +212,7 @@
assertThat(selectionRegistrar.sorted).isTrue()
// Act.
- selectionRegistrar.notifyPositionChange()
+ selectionRegistrar.notifyPositionChange(handlerId0)
// Assert.
assertThat(selectionRegistrar.sorted).isFalse()
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
index 4d87a55..63ae16a 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManagerTest.kt
@@ -147,7 +147,7 @@
fun TextFieldSelectionManager_touchSelectionObserver_onLongPress() {
whenever(layoutResultProxy.isPositionOnText(dragBeginPosition)).thenReturn(true)
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
assertThat(state.selectionIsOn).isTrue()
assertThat(state.showFloatingToolbar).isTrue()
@@ -176,7 +176,7 @@
whenever(layoutResultProxy.getLineEnd(fakeLineNumber)).thenReturn(fakeLineEnd)
// Act
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
// Assert
assertThat(state.selectionIsOn).isTrue()
@@ -195,7 +195,7 @@
@Test
fun TextFieldSelectionManager_touchSelectionObserver_onDrag() {
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
manager.touchSelectionObserver.onDrag(dragDistance)
assertThat(value.selection).isEqualTo(TextRange(0, text.length))
@@ -208,10 +208,10 @@
@Test
fun TextFieldSelectionManager_touchSelectionObserver_onStop() {
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
manager.touchSelectionObserver.onDrag(dragDistance)
- manager.touchSelectionObserver.onStop(Offset.Zero)
+ manager.touchSelectionObserver.onStop()
assertThat(state.showFloatingToolbar).isTrue()
}
@@ -246,9 +246,8 @@
fun TextFieldSelectionManager_handleDragObserver_onDrag_startHandle() {
manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
- val result = manager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
+ manager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
- assertThat(result).isEqualTo(dragDistance)
assertThat(state.showFloatingToolbar).isFalse()
assertThat(value.selection).isEqualTo(TextRange(dragOffset, "Hello".length))
verify(
@@ -261,9 +260,8 @@
fun TextFieldSelectionManager_handleDragObserver_onDrag_endHandle() {
manager.value = TextFieldValue(text = text, selection = TextRange(0, "Hello".length))
- val result = manager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
+ manager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
- assertThat(result).isEqualTo(dragDistance)
assertThat(state.showFloatingToolbar).isFalse()
assertThat(value.selection).isEqualTo(TextRange(0, dragOffset))
verify(
@@ -277,7 +275,7 @@
manager.handleDragObserver(false).onStart(Offset.Zero)
manager.handleDragObserver(false).onDrag(Offset.Zero)
- manager.handleDragObserver(false).onStop(Offset.Zero)
+ manager.handleDragObserver(false).onStop()
assertThat(state.draggingHandle).isFalse()
assertThat(state.showFloatingToolbar).isTrue()
@@ -502,7 +500,7 @@
@Test
fun isTextChanged_text_changed_return_true() {
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
manager.value = TextFieldValue(text + text)
assertThat(manager.isTextChanged()).isTrue()
@@ -510,7 +508,7 @@
@Test
fun isTextChanged_text_unchange_return_false() {
- manager.touchSelectionObserver.onLongPress(dragBeginPosition)
+ manager.touchSelectionObserver.onStart(dragBeginPosition)
assertThat(manager.isTextChanged()).isFalse()
}
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 9060916..4ba6f05 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
@@ -66,7 +66,6 @@
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withText
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -262,7 +261,6 @@
}
}
-@OptIn(ExperimentalCoroutinesApi::class)
private object TestingSnippets13 {
class MyTest() {
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
deleted file mode 100644
index 43a0f581..0000000
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * Macrobenchmark used for local validation of performance numbers coming from MacrobenchmarkRule.
- */
-@LargeTest
-@SdkSuppress(minSdkVersion = 29)
-@RunWith(Parameterized::class)
-class ProcessSpeedProfileValidation(
- private val compilationMode: CompilationMode,
- private val startupMode: StartupMode
-) {
- @get:Rule
- val benchmarkRule = MacrobenchmarkRule()
-
- @Test
- fun start() = benchmarkRule.measureRepeated(
- packageName = PACKAGE_NAME,
- metrics = listOf(StartupTimingMetric()),
- compilationMode = compilationMode,
- iterations = 3,
- startupMode = startupMode
- ) {
- pressHome()
- startActivityAndWait()
- }
-
- companion object {
- private const val PACKAGE_NAME = "androidx.compose.integration.macrobenchmark.target"
-
- @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
- @JvmStatic
- fun kilProcessParameters(): List<Array<Any>> {
- val compilationModes = listOf(
- CompilationMode.None,
- CompilationMode.SpeedProfile(warmupIterations = 3)
- )
- val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
- return compilationModes.zip(processKillOptions).map {
- arrayOf(it.first, it.second)
- }
- }
- }
-}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
index 1a35bf5..56696de 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -16,6 +16,9 @@
package androidx.compose.integration.macrobenchmark
+import androidx.benchmark.integration.macrobenchmark.createStartupCompilationParams
+import androidx.benchmark.integration.macrobenchmark.measureStartup
+import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -26,13 +29,16 @@
@LargeTest
@RunWith(Parameterized::class)
-class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+class SmallListStartupBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureStartup(
- profileCompiled = true,
+ compilationMode = compilationMode,
startupMode = startupMode
) {
action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
@@ -40,11 +46,8 @@
}
companion object {
- @Parameterized.Parameters(name = "mode={0}")
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters(): List<Array<Any>> {
- return listOf(StartupMode.COLD, StartupMode.WARM)
- .map { arrayOf(it) }
- }
+ fun parameters() = createStartupCompilationParams()
}
}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
index dfbb035..5f26ee6 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
@@ -14,39 +14,67 @@
* limitations under the License.
*/
-package androidx.compose.integration.macrobenchmark
+package androidx.benchmark.integration.macrobenchmark
import android.content.Intent
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.isSupportedWithVmSettings
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-const val TargetPackage = "androidx.compose.integration.macrobenchmark.target"
+const val TARGET_PACKAGE = "androidx.benchmark.integration.compose.target"
-/**
- * Simplified interface for standardizing e.g. package,
- * compilation types, and iteration count across project
- */
fun MacrobenchmarkRule.measureStartup(
- profileCompiled: Boolean,
+ compilationMode: CompilationMode,
startupMode: StartupMode,
- iterations: Int = 5,
+ iterations: Int = 3,
setupIntent: Intent.() -> Unit = {}
) = measureRepeated(
- packageName = TargetPackage,
+ packageName = TARGET_PACKAGE,
metrics = listOf(StartupTimingMetric()),
- compilationMode = if (profileCompiled) {
- CompilationMode.SpeedProfile(warmupIterations = 3)
- } else {
- CompilationMode.None
- },
+ compilationMode = compilationMode,
iterations = iterations,
startupMode = startupMode
) {
pressHome()
val intent = Intent()
- intent.setPackage(TargetPackage)
+ intent.setPackage(TARGET_PACKAGE)
setupIntent(intent)
startActivityAndWait(intent)
+}
+
+fun createStartupCompilationParams(
+ startupModes: List<StartupMode> = listOf(StartupMode.HOT, StartupMode.WARM, StartupMode.COLD),
+ compilationModes: List<CompilationMode> = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile()
+ )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+ for (startupMode in startupModes) {
+ for (compilationMode in compilationModes) {
+ // Skip configs that can't run, so they don't clutter Studio benchmark
+ // output with AssumptionViolatedException dumps
+ if (compilationMode.isSupportedWithVmSettings()) {
+ add(arrayOf(startupMode, compilationMode))
+ }
+ }
+ }
+}
+
+fun createCompilationParams(
+ compilationModes: List<CompilationMode> = listOf(
+ CompilationMode.None,
+ CompilationMode.Interpreted,
+ CompilationMode.SpeedProfile()
+ )
+): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+ for (compilationMode in compilationModes) {
+ // Skip configs that can't run, so they don't clutter Studio benchmark
+ // output with AssumptionViolatedException dumps
+ if (compilationMode.isSupportedWithVmSettings()) {
+ add(arrayOf(compilationMode))
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
similarity index 88%
rename from compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt
rename to compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
index bf96acd..0e97a25 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/FrameTimingMetricValidation.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialListScrollBenchmark.kt
@@ -18,6 +18,7 @@
import android.content.Intent
import android.graphics.Point
+import androidx.benchmark.integration.macrobenchmark.createCompilationParams
import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.FrameTimingMetric
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -34,7 +35,9 @@
@LargeTest
@RunWith(Parameterized::class)
-class FrameTimingMetricValidation(private val compilationMode: CompilationMode) {
+class TrivialListScrollBenchmark(
+ private val compilationMode: CompilationMode
+) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@@ -78,13 +81,8 @@
private const val COMPOSE_IDLE = "COMPOSE-IDLE"
- @Parameterized.Parameters(name = "compilation_mode={0}")
+ @Parameterized.Parameters(name = "compilation={0}")
@JvmStatic
- fun jankParameters(): List<Array<Any>> {
- return listOf(
- CompilationMode.None,
- CompilationMode.SpeedProfile(warmupIterations = 3)
- ).map { arrayOf(it) }
- }
+ fun parameters() = createCompilationParams()
}
}
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
index 9ebb6ed..3b0e076 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -16,6 +16,9 @@
package androidx.compose.integration.macrobenchmark
+import androidx.benchmark.integration.macrobenchmark.createStartupCompilationParams
+import androidx.benchmark.integration.macrobenchmark.measureStartup
+import androidx.benchmark.macro.CompilationMode
import androidx.benchmark.macro.StartupMode
import androidx.benchmark.macro.junit4.MacrobenchmarkRule
import androidx.test.filters.LargeTest
@@ -26,24 +29,24 @@
@LargeTest
@RunWith(Parameterized::class)
-class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+class TrivialStartupBenchmark(
+ private val startupMode: StartupMode,
+ private val compilationMode: CompilationMode
+) {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureStartup(
- profileCompiled = true,
+ compilationMode = compilationMode,
startupMode = startupMode
) {
action = "androidx.compose.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
}
companion object {
- @Parameterized.Parameters(name = "mode={0}")
+ @Parameterized.Parameters(name = "startup={0},compilation={1}")
@JvmStatic
- fun parameters(): List<Array<Any>> {
- return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
- .map { arrayOf(it) }
- }
+ fun parameters() = createStartupCompilationParams()
}
}
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
index 0c50d08..c3bde4f 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
@@ -30,6 +30,7 @@
val Composable = Name(PackageName, "Composable")
val CompositionLocal = Name(PackageName, "CompositionLocal")
+ val MutableState = Name(PackageName, "MutableState")
val MutableStateOf = Name(PackageName, "mutableStateOf")
val MutableStateListOf = Name(PackageName, "mutableStateListOf")
val MutableStateMapOf = Name(PackageName, "mutableStateMapOf")
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt b/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
index ccb3a74..ddd3859 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/PsiUtils.kt
@@ -19,6 +19,7 @@
import com.intellij.psi.PsiJavaFile
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
+import com.intellij.psi.util.InheritanceUtil
/**
* Returns whether [this] has [packageName] as its package name.
@@ -33,3 +34,9 @@
*/
val PsiMethod.returnsUnit
get() = returnType == PsiType.VOID
+
+/**
+ * @return whether [this] inherits from [name]. Returns `true` if [this] _is_ directly [name].
+ */
+fun PsiType.inheritsFrom(name: Name) =
+ InheritanceUtil.isInheritor(this, name.javaFqn)
\ No newline at end of file
diff --git a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
index 7e22dda..b583394 100644
--- a/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
+++ b/compose/lint/internal-lint-checks/src/main/java/androidx/compose/lint/ListIteratorDetector.kt
@@ -28,7 +28,6 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.impl.compiled.ClsMethodImpl
-import com.intellij.psi.util.InheritanceUtil
import kotlinx.metadata.KmClassifier
import org.jetbrains.kotlin.psi.KtForExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
@@ -54,7 +53,7 @@
// Type of the variable we are iterating on, i.e the type of `b` in `for (a in b)`
val iteratedValueType = node.iteratedValue.getExpressionType()
// We are iterating on a List
- if (InheritanceUtil.isInheritor(iteratedValueType, JavaList.javaFqn)) {
+ if (iteratedValueType?.inheritsFrom(JavaList) == true) {
// Find the `in` keyword to use as location
val inKeyword = (node.sourcePsi as? KtForExpression)?.inKeyword
val location = if (inKeyword == null) {
@@ -75,9 +74,7 @@
val receiverType = node.receiverType
// We are calling a method on a `List` type
- if (receiverType != null &&
- InheritanceUtil.isInheritor(node.receiverType, JavaList.javaFqn)
- ) {
+ if (receiverType?.inheritsFrom(JavaList) == true) {
when (val method = node.resolveToUElement()?.sourcePsi) {
// Parsing a class file
is ClsMethodImpl -> {
diff --git a/compose/material/material/api/1.0.0-beta04.txt b/compose/material/material/api/1.0.0-beta04.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/1.0.0-beta04.txt
+++ b/compose/material/material/api/1.0.0-beta04.txt
@@ -47,38 +47,6 @@
public final class BackdropScaffoldKt {
}
- public final class BottomDrawerState {
- ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
- method public boolean isClosed();
- method public boolean isExpanded();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- property public final boolean isClosed;
- property public final boolean isExpanded;
- property public final boolean isOpen;
- field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
- }
-
- public static final class BottomDrawerState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- }
-
- public enum BottomDrawerValue {
- enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
- }
-
public final class BottomNavigationDefaults {
method public float getElevation-D9Ej5fM();
property public final float Elevation;
@@ -243,24 +211,19 @@
public final class DrawerKt {
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/current.ignore b/compose/material/material/api/current.ignore
new file mode 100644
index 0000000..f5556ca
--- /dev/null
+++ b/compose/material/material/api/current.ignore
@@ -0,0 +1,25 @@
+// Baseline format: 1.0
+ChangedType: androidx.compose.material.DrawerState#getCurrentValue():
+ Method androidx.compose.material.DrawerState.getCurrentValue has changed return type from T to androidx.compose.material.DrawerValue
+
+
+RemovedClass: androidx.compose.material.BottomDrawerState:
+ Removed class androidx.compose.material.BottomDrawerState
+RemovedClass: androidx.compose.material.BottomDrawerValue:
+ Removed class androidx.compose.material.BottomDrawerValue
+
+
+RemovedMethod: androidx.compose.material.DrawerKt#rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>):
+ Removed method androidx.compose.material.DrawerKt.rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue,kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>)
+RemovedMethod: androidx.compose.material.DrawerState#getDirection():
+ Removed method androidx.compose.material.DrawerState.getDirection()
+RemovedMethod: androidx.compose.material.DrawerState#getOffset():
+ Removed method androidx.compose.material.DrawerState.getOffset()
+RemovedMethod: androidx.compose.material.DrawerState#getOverflow():
+ Removed method androidx.compose.material.DrawerState.getOverflow()
+RemovedMethod: androidx.compose.material.DrawerState#getTargetValue():
+ Removed method androidx.compose.material.DrawerState.getTargetValue()
+RemovedMethod: androidx.compose.material.DrawerState#performDrag(float):
+ Removed method androidx.compose.material.DrawerState.performDrag(float)
+RemovedMethod: androidx.compose.material.DrawerState#performFling(float, kotlin.coroutines.Continuation<? super kotlin.Unit>):
+ Removed method androidx.compose.material.DrawerState.performFling(float,kotlin.coroutines.Continuation<? super kotlin.Unit>)
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -47,38 +47,6 @@
public final class BackdropScaffoldKt {
}
- public final class BottomDrawerState {
- ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
- method public boolean isClosed();
- method public boolean isExpanded();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- property public final boolean isClosed;
- property public final boolean isExpanded;
- property public final boolean isOpen;
- field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
- }
-
- public static final class BottomDrawerState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- }
-
- public enum BottomDrawerValue {
- enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
- }
-
public final class BottomNavigationDefaults {
method public float getElevation-D9Ej5fM();
property public final float Elevation;
@@ -243,24 +211,19 @@
public final class DrawerKt {
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt b/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
index 2795abb..ab08c2c 100644
--- a/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/material/material/api/public_plus_experimental_1.0.0-beta04.txt
@@ -71,7 +71,7 @@
enum_constant public static final androidx.compose.material.BackdropValue Revealed;
}
- public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
+ @androidx.compose.material.ExperimentalMaterialApi public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
@@ -89,7 +89,7 @@
method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
}
- public enum BottomDrawerValue {
+ @androidx.compose.material.ExperimentalMaterialApi public enum BottomDrawerValue {
enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
@@ -307,18 +307,28 @@
public final class DrawerKt {
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomDrawer--6CoO6E(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomDrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
- @androidx.compose.runtime.Stable public final class DrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.DrawerValue> {
+ @androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
+ method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+ method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? snapTo(androidx.compose.material.DrawerValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
+ property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+ property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
field public static final androidx.compose.material.DrawerState.Companion Companion;
}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 2795abb..ab08c2c 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -71,7 +71,7 @@
enum_constant public static final androidx.compose.material.BackdropValue Revealed;
}
- public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
+ @androidx.compose.material.ExperimentalMaterialApi public final class BottomDrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomDrawerValue> {
ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
@@ -89,7 +89,7 @@
method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
}
- public enum BottomDrawerValue {
+ @androidx.compose.material.ExperimentalMaterialApi public enum BottomDrawerValue {
enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
@@ -307,18 +307,28 @@
public final class DrawerKt {
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomDrawer--6CoO6E(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomDrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
+ method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
- @androidx.compose.runtime.Stable public final class DrawerState extends androidx.compose.material.SwipeableState<androidx.compose.material.DrawerValue> {
+ @androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
+ method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+ method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? snapTo(androidx.compose.material.DrawerValue targetValue, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
+ property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+ property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
field public static final androidx.compose.material.DrawerState.Companion Companion;
}
diff --git a/compose/material/material/api/restricted_1.0.0-beta04.txt b/compose/material/material/api/restricted_1.0.0-beta04.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/restricted_1.0.0-beta04.txt
+++ b/compose/material/material/api/restricted_1.0.0-beta04.txt
@@ -47,38 +47,6 @@
public final class BackdropScaffoldKt {
}
- public final class BottomDrawerState {
- ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
- method public boolean isClosed();
- method public boolean isExpanded();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- property public final boolean isClosed;
- property public final boolean isExpanded;
- property public final boolean isOpen;
- field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
- }
-
- public static final class BottomDrawerState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- }
-
- public enum BottomDrawerValue {
- enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
- }
-
public final class BottomNavigationDefaults {
method public float getElevation-D9Ej5fM();
property public final float Elevation;
@@ -243,24 +211,19 @@
public final class DrawerKt {
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/api/restricted_current.ignore b/compose/material/material/api/restricted_current.ignore
new file mode 100644
index 0000000..f5556ca
--- /dev/null
+++ b/compose/material/material/api/restricted_current.ignore
@@ -0,0 +1,25 @@
+// Baseline format: 1.0
+ChangedType: androidx.compose.material.DrawerState#getCurrentValue():
+ Method androidx.compose.material.DrawerState.getCurrentValue has changed return type from T to androidx.compose.material.DrawerValue
+
+
+RemovedClass: androidx.compose.material.BottomDrawerState:
+ Removed class androidx.compose.material.BottomDrawerState
+RemovedClass: androidx.compose.material.BottomDrawerValue:
+ Removed class androidx.compose.material.BottomDrawerValue
+
+
+RemovedMethod: androidx.compose.material.DrawerKt#rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>):
+ Removed method androidx.compose.material.DrawerKt.rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue,kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean>)
+RemovedMethod: androidx.compose.material.DrawerState#getDirection():
+ Removed method androidx.compose.material.DrawerState.getDirection()
+RemovedMethod: androidx.compose.material.DrawerState#getOffset():
+ Removed method androidx.compose.material.DrawerState.getOffset()
+RemovedMethod: androidx.compose.material.DrawerState#getOverflow():
+ Removed method androidx.compose.material.DrawerState.getOverflow()
+RemovedMethod: androidx.compose.material.DrawerState#getTargetValue():
+ Removed method androidx.compose.material.DrawerState.getTargetValue()
+RemovedMethod: androidx.compose.material.DrawerState#performDrag(float):
+ Removed method androidx.compose.material.DrawerState.performDrag(float)
+RemovedMethod: androidx.compose.material.DrawerState#performFling(float, kotlin.coroutines.Continuation<? super kotlin.Unit>):
+ Removed method androidx.compose.material.DrawerState.performFling(float,kotlin.coroutines.Continuation<? super kotlin.Unit>)
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index c630e0c..7cedeb6 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -47,38 +47,6 @@
public final class BackdropScaffoldKt {
}
- public final class BottomDrawerState {
- ctor public BottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
- method public boolean isClosed();
- method public boolean isExpanded();
- method public boolean isOpen();
- method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- property public final boolean isClosed;
- property public final boolean isExpanded;
- property public final boolean isOpen;
- field public static final androidx.compose.material.BottomDrawerState.Companion Companion;
- }
-
- public static final class BottomDrawerState.Companion {
- method public androidx.compose.runtime.saveable.Saver<androidx.compose.material.BottomDrawerState,androidx.compose.material.BottomDrawerValue> Saver(kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
- }
-
- public enum BottomDrawerValue {
- enum_constant public static final androidx.compose.material.BottomDrawerValue Closed;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Expanded;
- enum_constant public static final androidx.compose.material.BottomDrawerValue Open;
- }
-
public final class BottomNavigationDefaults {
method public float getElevation-D9Ej5fM();
property public final float Elevation;
@@ -243,24 +211,19 @@
public final class DrawerKt {
method @androidx.compose.runtime.Composable public static void ModalDrawer-TlzqArY(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> drawerContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.DrawerState drawerState, optional boolean gesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
- method @androidx.compose.runtime.Composable public static androidx.compose.material.BottomDrawerState rememberBottomDrawerState(androidx.compose.material.BottomDrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomDrawerValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.runtime.Composable public static androidx.compose.material.DrawerState rememberDrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
}
@androidx.compose.runtime.Stable public final class DrawerState {
ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final T! getCurrentValue();
- method public final float getDirection();
- method public final androidx.compose.runtime.State<java.lang.Float> getOffset();
- method public final androidx.compose.runtime.State<java.lang.Float> getOverflow();
- method public final T! getTargetValue();
- method public final boolean isAnimationRunning();
+ method public androidx.compose.material.DrawerValue getCurrentValue();
+ method public boolean isAnimationRunning();
method public boolean isClosed();
method public boolean isOpen();
method public suspend Object? open(kotlin.coroutines.Continuation<? super kotlin.Unit> p);
- method public final float performDrag(float delta);
- method public final suspend Object? performFling(float velocity, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+ property public final androidx.compose.material.DrawerValue currentValue;
+ property public final boolean isAnimationRunning;
property public final boolean isClosed;
property public final boolean isOpen;
field public static final androidx.compose.material.DrawerState.Companion Companion;
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
index d9d9983..c474e24 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/ImageVectorGenerator.kt
@@ -58,7 +58,6 @@
// Kotlin 1.4) each property with the same name will be considered as a possible candidate
// for resolution, regardless of the access modifier, so by using unique names we reduce
// the size from ~6000 to 1, and speed up compilation time for these icons.
- @OptIn(ExperimentalStdlibApi::class)
val backingPropertyName = "_" + iconName.decapitalize(Locale.ROOT)
val backingProperty = backingProperty(name = backingPropertyName)
return FileSpec.builder(
diff --git a/compose/material/material/integration-tests/material-catalog/build.gradle b/compose/material/material/integration-tests/material-catalog/build.gradle
index 3cf2268..3b1ed12 100644
--- a/compose/material/material/integration-tests/material-catalog/build.gradle
+++ b/compose/material/material/integration-tests/material-catalog/build.gradle
@@ -28,20 +28,17 @@
kotlinPlugin project(":compose:compiler:compiler")
implementation(KOTLIN_STDLIB)
- implementation project(":compose:foundation:foundation")
- implementation project(":compose:foundation:foundation-layout")
implementation project(":compose:integration-tests:demos:common")
+ implementation project(":compose:ui:ui")
implementation project(":compose:material:material")
implementation project(":compose:material:material:material-samples")
- implementation project(":compose:runtime:runtime")
- implementation project(":compose:ui:ui")
- implementation project(":compose:ui:ui-text")
implementation project(":activity:activity-compose")
+ implementation project(":navigation:navigation-compose")
}
androidx {
- name = "Compose Material Catalog"
+ name = "Compose Material catalog"
publish = Publish.NONE
inceptionYear = "2021"
- description = "This is a project for a Material catalog."
+ description = "This is a project for a Compose Material catalog."
}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml b/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
index 40c6046..d6ee456 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
+++ b/compose/material/material/integration-tests/material-catalog/src/main/AndroidManifest.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2021 The Android Open Source Project
~
@@ -20,9 +21,8 @@
<application>
<activity android:name=".CatalogActivity"
- android:theme="@android:style/Theme.NoTitleBar"
- android:configChanges="orientation|screenSize"
- android:label="Catalog">
+ android:theme="@style/Theme.Catalog"
+ android:label="@string/app_name">
</activity>
</application>
</manifest>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
index 55c1ac1..580b0f5 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogActivity.kt
@@ -19,38 +19,14 @@
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.Text
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
+import androidx.core.view.WindowCompat
class CatalogActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
CatalogApp()
}
}
}
-
-@Composable
-fun CatalogApp() {
- val colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
- MaterialTheme(colors = colors) {
- Surface(modifier = Modifier.fillMaxSize()) {
- Box {
- Text(
- text = "Nothing to see here!",
- modifier = Modifier.align(Alignment.Center)
- )
- }
- }
- }
-}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
similarity index 69%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
index 5ece3c2..503ec64 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogApp.kt
@@ -16,6 +16,15 @@
package androidx.compose.material.catalog
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.compose.material.catalog.insets.ProvideWindowInsets
+import androidx.compose.material.catalog.ui.theme.CatalogTheme
+import androidx.compose.runtime.Composable
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+@Composable
+fun CatalogApp() {
+ ProvideWindowInsets {
+ CatalogTheme {
+ NavGraph()
+ }
+ }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
similarity index 91%
rename from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
rename to compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
index 5ece3c2..febf878 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/CatalogDemo.kt
@@ -18,4 +18,4 @@
import androidx.compose.integration.demos.common.ActivityDemo
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+val MaterialCatalog = ActivityDemo("Material catalog", CatalogActivity::class)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt
new file mode 100644
index 0000000..62d0076
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/NavGraph.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.material.catalog
+
+import androidx.compose.material.catalog.model.Components
+import androidx.compose.material.catalog.ui.component.Component
+import androidx.compose.material.catalog.ui.example.Example
+import androidx.compose.material.catalog.ui.home.Home
+import androidx.compose.runtime.Composable
+import androidx.navigation.NavType
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.navArgument
+import androidx.navigation.compose.navigate
+import androidx.navigation.compose.rememberNavController
+
+@Composable
+fun NavGraph() {
+ val navController = rememberNavController()
+ NavHost(
+ navController = navController,
+ startDestination = HOME_ROUTE
+ ) {
+ composable(HOME_ROUTE) {
+ Home(
+ components = Components,
+ onComponentClick = { component ->
+ val componentId = component.id
+ val route = "$COMPONENT_ROUTE/$componentId"
+ navController.navigate(route)
+ }
+ )
+ }
+ composable(
+ route = "$COMPONENT_ROUTE/" +
+ "{$COMPONENT_ID_ARG_NAME}",
+ arguments = listOf(
+ navArgument(COMPONENT_ID_ARG_NAME) { type = NavType.IntType }
+ )
+ ) { navBackStackEntry ->
+ val arguments = requireNotNull(navBackStackEntry.arguments) { "No arguments" }
+ val componentId = arguments.getInt(COMPONENT_ID_ARG_NAME)
+ val component = Components.first { component -> component.id == componentId }
+ Component(
+ component = component,
+ onExampleClick = { example ->
+ val exampleIndex = component.examples.indexOf(example)
+ val route = "$EXAMPLE_ROUTE/$componentId/$exampleIndex"
+ navController.navigate(route)
+ },
+ onBackClick = { navController.popBackStack() }
+ )
+ }
+ composable(
+ route = "$EXAMPLE_ROUTE/" +
+ "{$COMPONENT_ID_ARG_NAME}/" +
+ "{$EXAMPLE_INDEX_ARG_NAME}",
+ arguments = listOf(
+ navArgument(COMPONENT_ID_ARG_NAME) { type = NavType.IntType },
+ navArgument(EXAMPLE_INDEX_ARG_NAME) { type = NavType.IntType }
+ )
+ ) { navBackStackEntry ->
+ val arguments = requireNotNull(navBackStackEntry.arguments) { "No arguments" }
+ val componentId = arguments.getInt(COMPONENT_ID_ARG_NAME)
+ val exampleIndex = arguments.getInt(EXAMPLE_INDEX_ARG_NAME)
+ val component = Components.first { component -> component.id == componentId }
+ val example = component.examples[exampleIndex]
+ Example(
+ example = example,
+ onBackClick = { navController.popBackStack() }
+ )
+ }
+ }
+}
+
+private const val HOME_ROUTE = "home"
+private const val COMPONENT_ROUTE = "component"
+private const val EXAMPLE_ROUTE = "example"
+private const val COMPONENT_ID_ARG_NAME = "componentId"
+private const val EXAMPLE_INDEX_ARG_NAME = "exampleIndex"
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt
new file mode 100644
index 0000000..cd8110f
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Insets.kt
@@ -0,0 +1,644 @@
+/*
+ * 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.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Insets.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused", "PropertyName")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import android.view.View
+import android.view.WindowInsetsAnimation
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+
+/**
+ * Main holder of our inset values.
+ */
+@Stable
+class WindowInsets {
+ /**
+ * Inset values which match [WindowInsetsCompat.Type.systemBars]
+ */
+ val systemBars: InsetsType = InsetsType()
+
+ /**
+ * Inset values which match [WindowInsetsCompat.Type.systemGestures]
+ */
+ val systemGestures: InsetsType = InsetsType()
+
+ /**
+ * Inset values which match [WindowInsetsCompat.Type.navigationBars]
+ */
+ val navigationBars: InsetsType = InsetsType()
+
+ /**
+ * Inset values which match [WindowInsetsCompat.Type.statusBars]
+ */
+ val statusBars: InsetsType = InsetsType()
+
+ /**
+ * Inset values which match [WindowInsetsCompat.Type.ime]
+ */
+ val ime: InsetsType = InsetsType()
+}
+
+/**
+ * Represents the values for a type of insets, and stores information about the layout insets,
+ * animating insets, and visibility of the insets.
+ *
+ * [InsetsType] instances are commonly stored in a [WindowInsets] instance.
+ */
+@Stable
+@Suppress("MemberVisibilityCanBePrivate")
+class InsetsType : Insets {
+ private var ongoingAnimationsCount by mutableStateOf(0)
+ internal val _layoutInsets = MutableInsets()
+ internal val _animatedInsets = MutableInsets()
+
+ /**
+ * The layout insets for this [InsetsType]. These are the insets which are defined from the
+ * current window layout.
+ *
+ * You should not normally need to use this directly, and instead use [left], [top],
+ * [right], and [bottom] to return the correct value for the current state.
+ */
+ val layoutInsets: Insets
+ get() = _layoutInsets
+
+ /**
+ * The animated insets for this [InsetsType]. These are the insets which are updated from
+ * any on-going animations. If there are no animations in progress, the returned [Insets] will
+ * be empty.
+ *
+ * You should not normally need to use this directly, and instead use [left], [top],
+ * [right], and [bottom] to return the correct value for the current state.
+ */
+ val animatedInsets: Insets
+ get() = _animatedInsets
+
+ /**
+ * The left dimension of the insets in pixels.
+ */
+ override val left: Int
+ get() = (if (animationInProgress) animatedInsets else layoutInsets).left
+
+ /**
+ * The top dimension of the insets in pixels.
+ */
+ override val top: Int
+ get() = (if (animationInProgress) animatedInsets else layoutInsets).top
+
+ /**
+ * The right dimension of the insets in pixels.
+ */
+ override val right: Int
+ get() = (if (animationInProgress) animatedInsets else layoutInsets).right
+
+ /**
+ * The bottom dimension of the insets in pixels.
+ */
+ override val bottom: Int
+ get() = (if (animationInProgress) animatedInsets else layoutInsets).bottom
+
+ /**
+ * Whether the insets are currently visible.
+ */
+ var isVisible by mutableStateOf(true)
+ internal set
+
+ /**
+ * Whether this insets type is being animated at this moment.
+ */
+ val animationInProgress: Boolean
+ get() = ongoingAnimationsCount > 0
+
+ /**
+ * The progress of any ongoing animations, in the range of 0 to 1.
+ * If there is no animation in progress, this will return 0.
+ */
+ @get:FloatRange(from = 0.0, to = 1.0)
+ var animationFraction by mutableStateOf(0f)
+ internal set
+
+ internal fun onAnimationStart() {
+ ongoingAnimationsCount++
+ }
+
+ internal fun onAnimationEnd() {
+ ongoingAnimationsCount--
+
+ if (ongoingAnimationsCount == 0) {
+ // If there are no on-going animations, clear out the animated insets
+ _animatedInsets.reset()
+ animationFraction = 0f
+ }
+ }
+}
+
+@Stable
+interface Insets {
+ /**
+ * The left dimension of these insets in pixels.
+ */
+ @get:IntRange(from = 0)
+ val left: Int
+
+ /**
+ * The top dimension of these insets in pixels.
+ */
+ @get:IntRange(from = 0)
+ val top: Int
+
+ /**
+ * The right dimension of these insets in pixels.
+ */
+ @get:IntRange(from = 0)
+ val right: Int
+
+ /**
+ * The bottom dimension of these insets in pixels.
+ */
+ @get:IntRange(from = 0)
+ val bottom: Int
+
+ fun copy(
+ left: Int = this.left,
+ top: Int = this.top,
+ right: Int = this.right,
+ bottom: Int = this.bottom,
+ ): Insets = MutableInsets(left, top, right, bottom)
+
+ operator fun minus(other: Insets): Insets = copy(
+ left = this.left - other.left,
+ top = this.top - other.top,
+ right = this.right - other.right,
+ bottom = this.bottom - other.bottom,
+ )
+
+ operator fun plus(other: Insets): Insets = copy(
+ left = this.left + other.left,
+ top = this.top + other.top,
+ right = this.right + other.right,
+ bottom = this.bottom + other.bottom,
+ )
+}
+
+internal class MutableInsets(
+ left: Int = 0,
+ top: Int = 0,
+ right: Int = 0,
+ bottom: Int = 0,
+) : Insets {
+ override var left by mutableStateOf(left)
+ internal set
+
+ override var top by mutableStateOf(top)
+ internal set
+
+ override var right by mutableStateOf(right)
+ internal set
+
+ override var bottom by mutableStateOf(bottom)
+ internal set
+
+ fun reset() {
+ left = 0
+ top = 0
+ right = 0
+ bottom = 0
+ }
+}
+
+/**
+ * Composition local containing the current [WindowInsets].
+ */
+val LocalWindowInsets = staticCompositionLocalOf { WindowInsets() }
+
+/**
+ * This class sets up the necessary listeners on the given [view] to be able to observe
+ * [WindowInsetsCompat] instances dispatched by the system.
+ *
+ * This class is useful for when you prefer to handle the ownership of the [WindowInsets]
+ * yourself. One example of this is if you find yourself using [ProvideWindowInsets] in fragments.
+ *
+ * It is convenient to use [ProvideWindowInsets] in fragments, but that can result in a
+ * delay in the initial inset update, which results in a visual flicker.
+ * See [this issue](https://github.com/google/accompanist/issues/155) for more information.
+ *
+ * The alternative is for fragments to manage the [WindowInsets] themselves, like so:
+ *
+ * ```
+ * override fun onCreateView(
+ * inflater: LayoutInflater,
+ * container: ViewGroup?,
+ * savedInstanceState: Bundle?
+ * ): View = ComposeView(requireContext()).apply {
+ * layoutParams = LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ *
+ * // Create an ViewWindowInsetObserver using this view
+ * val observer = ViewWindowInsetObserver(this)
+ *
+ * // Call start() to start listening now.
+ * // The WindowInsets instance is returned to us.
+ * val windowInsets = observer.start()
+ *
+ * setContent {
+ * // Instead of calling ProvideWindowInsets, we use CompositionLocalProvider to provide
+ * // the WindowInsets instance from above to LocalWindowInsets
+ * CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+ * /* Content */
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param view The view to observe [WindowInsetsCompat]s from.
+ */
+class ViewWindowInsetObserver(private val view: View) {
+ private val attachListener = object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets()
+ override fun onViewDetachedFromWindow(v: View) = Unit
+ }
+
+ /**
+ * Whether this [ViewWindowInsetObserver] is currently observing.
+ */
+ @Suppress("MemberVisibilityCanBePrivate")
+ var isObserving: Boolean = false
+ private set
+
+ /**
+ * Start observing window insets from [view]. Make sure to call [stop] if required.
+ *
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are
+ * dispatched to the host view. Defaults to `true`.
+ */
+ fun start(
+ consumeWindowInsets: Boolean = true
+ ): WindowInsets {
+ return WindowInsets().apply {
+ observeInto(
+ windowInsets = this,
+ consumeWindowInsets = consumeWindowInsets,
+ windowInsetsAnimationsEnabled = false
+ )
+ }
+ }
+
+ /**
+ * Start observing window insets from [view]. Make sure to call [stop] if required.
+ *
+ * @param windowInsetsAnimationsEnabled Whether to listen for [WindowInsetsAnimation]s, such as
+ * IME animations.
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are
+ * dispatched to the host view. Defaults to `true`.
+ */
+ @ExperimentalAnimatedInsets
+ fun start(
+ windowInsetsAnimationsEnabled: Boolean,
+ consumeWindowInsets: Boolean = true,
+ ): WindowInsets {
+ return WindowInsets().apply {
+ observeInto(
+ windowInsets = this,
+ consumeWindowInsets = consumeWindowInsets,
+ windowInsetsAnimationsEnabled = windowInsetsAnimationsEnabled
+ )
+ }
+ }
+
+ internal fun observeInto(
+ windowInsets: WindowInsets,
+ consumeWindowInsets: Boolean,
+ windowInsetsAnimationsEnabled: Boolean,
+ ) {
+ require(!isObserving) {
+ "start() called, but this ViewWindowInsetObserver is already observing"
+ }
+
+ ViewCompat.setOnApplyWindowInsetsListener(view) { _, wic ->
+ // Go through each inset type and update its layoutInsets from the
+ // WindowInsetsCompat values
+ windowInsets.statusBars.run {
+ _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.statusBars()))
+ isVisible = wic.isVisible(WindowInsetsCompat.Type.statusBars())
+ }
+ windowInsets.navigationBars.run {
+ _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.navigationBars()))
+ isVisible = wic.isVisible(WindowInsetsCompat.Type.navigationBars())
+ }
+ windowInsets.systemBars.run {
+ _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.systemBars()))
+ isVisible = wic.isVisible(WindowInsetsCompat.Type.systemBars())
+ }
+ windowInsets.systemGestures.run {
+ _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.systemGestures()))
+ isVisible = wic.isVisible(WindowInsetsCompat.Type.systemGestures())
+ }
+ windowInsets.ime.run {
+ _layoutInsets.updateFrom(wic.getInsets(WindowInsetsCompat.Type.ime()))
+ isVisible = wic.isVisible(WindowInsetsCompat.Type.ime())
+ }
+
+ if (consumeWindowInsets) WindowInsetsCompat.CONSUMED else wic
+ }
+
+ // Add an OnAttachStateChangeListener to request an inset pass each time we're attached
+ // to the window
+ val attachListener = object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) = v.requestApplyInsets()
+ override fun onViewDetachedFromWindow(v: View) = Unit
+ }
+ view.addOnAttachStateChangeListener(attachListener)
+
+ if (windowInsetsAnimationsEnabled) {
+ ViewCompat.setWindowInsetsAnimationCallback(
+ view,
+ InnerWindowInsetsAnimationCallback(windowInsets)
+ )
+ } else {
+ ViewCompat.setWindowInsetsAnimationCallback(view, null)
+ }
+
+ if (view.isAttachedToWindow) {
+ // If the view is already attached, we can request an inset pass now
+ view.requestApplyInsets()
+ }
+
+ isObserving = true
+ }
+
+ /**
+ * Removes any listeners from the [view] so that we no longer observe inset changes.
+ *
+ * This is only required to be called from hosts which have a shorter lifetime than the [view].
+ * For example, if you're using [ViewWindowInsetObserver] from a `@Composable` function,
+ * you should call [stop] from an `onDispose` block, like so:
+ *
+ * ```
+ * DisposableEffect(view) {
+ * val observer = ViewWindowInsetObserver(view)
+ * // ...
+ * onDispose {
+ * observer.stop()
+ * }
+ * }
+ * ```
+ *
+ * Whereas if you're using this class from a fragment (or similar), it is not required to
+ * call this function since it will live as least as longer as the view.
+ */
+ fun stop() {
+ require(isObserving) {
+ "stop() called, but this ViewWindowInsetObserver is not currently observing"
+ }
+ view.removeOnAttachStateChangeListener(attachListener)
+ ViewCompat.setOnApplyWindowInsetsListener(view, null)
+ isObserving = false
+ }
+}
+
+/**
+ * Applies any [WindowInsetsCompat] values to [LocalWindowInsets], which are then available
+ * within [content].
+ *
+ * If you're using this in fragments, you may wish to take a look at
+ * [ViewWindowInsetObserver] for a more optimal solution.
+ *
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to
+ * the host view. Defaults to `true`.
+ */
+@SuppressLint("UnnecessaryLambdaCreation")
+@Composable
+fun ProvideWindowInsets(
+ consumeWindowInsets: Boolean = true,
+ content: @Composable () -> Unit,
+) {
+ val view = LocalView.current
+ val windowInsets = LocalWindowInsets.current
+
+ DisposableEffect(view) {
+ val observer = ViewWindowInsetObserver(view)
+ observer.observeInto(
+ windowInsets = windowInsets,
+ consumeWindowInsets = consumeWindowInsets,
+ windowInsetsAnimationsEnabled = false
+ )
+ onDispose {
+ observer.stop()
+ }
+ }
+
+ CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+ content()
+ }
+}
+
+/**
+ * Applies any [WindowInsetsCompat] values to [LocalWindowInsets], which are then available
+ * within [content].
+ *
+ * If you're using this in fragments, you may wish to take a look at
+ * [ViewWindowInsetObserver] for a more optimal solution.
+ *
+ * @param windowInsetsAnimationsEnabled Whether to listen for [WindowInsetsAnimation]s, such as
+ * IME animations.
+ * @param consumeWindowInsets Whether to consume any [WindowInsetsCompat]s which are dispatched to
+ * the host view. Defaults to `true`.
+ */
+@SuppressLint("UnnecessaryLambdaCreation")
+@ExperimentalAnimatedInsets
+@Composable
+fun ProvideWindowInsets(
+ windowInsetsAnimationsEnabled: Boolean,
+ consumeWindowInsets: Boolean = true,
+ content: @Composable () -> Unit
+) {
+ val view = LocalView.current
+ val windowInsets = remember { WindowInsets() }
+
+ DisposableEffect(view) {
+ val observer = ViewWindowInsetObserver(view)
+ observer.observeInto(
+ windowInsets = windowInsets,
+ consumeWindowInsets = consumeWindowInsets,
+ windowInsetsAnimationsEnabled = windowInsetsAnimationsEnabled
+ )
+ onDispose {
+ observer.stop()
+ }
+ }
+
+ CompositionLocalProvider(LocalWindowInsets provides windowInsets) {
+ content()
+ }
+}
+
+private class InnerWindowInsetsAnimationCallback(
+ private val windowInsets: WindowInsets,
+) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+ override fun onPrepare(animation: WindowInsetsAnimationCompat) {
+ // Go through each type and flag that an animation has started
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
+ windowInsets.ime.onAnimationStart()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.statusBars() != 0) {
+ windowInsets.statusBars.onAnimationStart()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.navigationBars() != 0) {
+ windowInsets.navigationBars.onAnimationStart()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.systemBars() != 0) {
+ windowInsets.systemBars.onAnimationStart()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.systemGestures() != 0) {
+ windowInsets.systemGestures.onAnimationStart()
+ }
+ }
+
+ override fun onProgress(
+ platformInsets: WindowInsetsCompat,
+ runningAnimations: List<WindowInsetsAnimationCompat>
+ ): WindowInsetsCompat {
+ // Update each inset type with the given parameters
+ windowInsets.ime.updateAnimation(
+ platformInsets = platformInsets,
+ runningAnimations = runningAnimations,
+ type = WindowInsetsCompat.Type.ime()
+ )
+ windowInsets.statusBars.updateAnimation(
+ platformInsets = platformInsets,
+ runningAnimations = runningAnimations,
+ type = WindowInsetsCompat.Type.statusBars()
+ )
+ windowInsets.navigationBars.updateAnimation(
+ platformInsets = platformInsets,
+ runningAnimations = runningAnimations,
+ type = WindowInsetsCompat.Type.navigationBars()
+ )
+ windowInsets.systemBars.updateAnimation(
+ platformInsets = platformInsets,
+ runningAnimations = runningAnimations,
+ type = WindowInsetsCompat.Type.systemBars()
+ )
+ windowInsets.systemBars.updateAnimation(
+ platformInsets = platformInsets,
+ runningAnimations = runningAnimations,
+ type = WindowInsetsCompat.Type.systemGestures()
+ )
+ return platformInsets
+ }
+
+ private inline fun InsetsType.updateAnimation(
+ platformInsets: WindowInsetsCompat,
+ runningAnimations: List<WindowInsetsAnimationCompat>,
+ type: Int,
+ ) {
+ // If there are animations of the given type...
+ if (runningAnimations.any { it.typeMask or type != 0 }) {
+ // Update our animated inset values
+ _animatedInsets.updateFrom(platformInsets.getInsets(type))
+ // And update the animation fraction. We use the maximum animation progress of any
+ // ongoing animations for this type.
+ animationFraction = runningAnimations.maxOf { it.fraction }
+ }
+ }
+
+ override fun onEnd(animation: WindowInsetsAnimationCompat) {
+ // Go through each type and flag that an animation has ended
+ if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
+ windowInsets.ime.onAnimationEnd()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.statusBars() != 0) {
+ windowInsets.statusBars.onAnimationEnd()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.navigationBars() != 0) {
+ windowInsets.navigationBars.onAnimationEnd()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.systemBars() != 0) {
+ windowInsets.systemBars.onAnimationEnd()
+ }
+ if (animation.typeMask and WindowInsetsCompat.Type.systemGestures() != 0) {
+ windowInsets.systemGestures.onAnimationEnd()
+ }
+ }
+}
+
+/**
+ * Updates our mutable state backed [InsetsType] from an Android system insets.
+ */
+private fun MutableInsets.updateFrom(insets: androidx.core.graphics.Insets) {
+ left = insets.left
+ top = insets.top
+ right = insets.right
+ bottom = insets.bottom
+}
+
+/**
+ * Ensures that each dimension is not less than corresponding dimension in the
+ * specified [minimumValue].
+ *
+ * @return this if every dimension is greater than or equal to the corresponding
+ * dimension value in [minimumValue], otherwise a copy of this with each dimension coerced with the
+ * corresponding dimension value in [minimumValue].
+ */
+fun InsetsType.coerceEachDimensionAtLeast(minimumValue: InsetsType): Insets {
+ // Fast path, no need to copy if: this >= minimumValue
+ if (left >= minimumValue.left && top >= minimumValue.top &&
+ right >= minimumValue.right && bottom >= minimumValue.bottom
+ ) {
+ return this
+ }
+ return MutableInsets(
+ left = left.coerceAtLeast(minimumValue.left),
+ top = top.coerceAtLeast(minimumValue.top),
+ right = right.coerceAtLeast(minimumValue.right),
+ bottom = bottom.coerceAtLeast(minimumValue.bottom),
+ )
+}
+
+enum class HorizontalSide { Left, Right }
+enum class VerticalSide { Top, Bottom }
+
+@RequiresOptIn(
+ message = "Animated Insets support is experimental. The API may be changed in the " +
+ "future."
+)
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalAnimatedInsets
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt
new file mode 100644
index 0000000..c3fcfe6
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Padding.kt
@@ -0,0 +1,248 @@
+/*
+ * 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.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Padding.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+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 androidx.compose.ui.unit.offset
+
+/**
+ * Selectively apply additional space which matches the width/height of any system bars present
+ * on the respective edges of the screen.
+ *
+ * @param enabled Whether to apply padding using the system bars dimensions on the respective edges.
+ * Defaults to `true`.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.systemBarsPadding(
+ enabled: Boolean = true
+): Modifier = composed {
+ InsetsPaddingModifier(
+ insetsType = LocalWindowInsets.current.systemBars,
+ applyLeft = enabled,
+ applyTop = enabled,
+ applyRight = enabled,
+ applyBottom = enabled
+ )
+}
+
+/**
+ * Apply additional space which matches the height of the status bars height along the top edge
+ * of the content.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.statusBarsPadding(): Modifier = composed {
+ InsetsPaddingModifier(
+ insetsType = LocalWindowInsets.current.statusBars,
+ applyTop = true
+ )
+}
+
+/**
+ * Apply additional space which matches the height of the navigation bars height
+ * along the [bottom] edge of the content, and additional space which matches the width of
+ * the navigation bars on the respective [left] and [right] edges.
+ *
+ * @param bottom Whether to apply padding to the bottom edge, which matches the navigation bars
+ * height (if present) at the bottom edge of the screen. Defaults to `true`.
+ * @param left Whether to apply padding to the left edge, which matches the navigation bars width
+ * (if present) on the left edge of the screen. Defaults to `true`.
+ * @param right Whether to apply padding to the right edge, which matches the navigation bars width
+ * (if present) on the right edge of the screen. Defaults to `true`.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsPadding(
+ bottom: Boolean = true,
+ left: Boolean = true,
+ right: Boolean = true
+): Modifier = composed {
+ InsetsPaddingModifier(
+ insetsType = LocalWindowInsets.current.navigationBars,
+ applyLeft = left,
+ applyRight = right,
+ applyBottom = bottom
+ )
+}
+
+/**
+ * Apply additional space which matches the height of the [WindowInsets.ime] (on-screen keyboard)
+ * height along the bottom edge of the content.
+ *
+ * This method has no special handling for the [WindowInsets.navigationBars], which usually
+ * intersect the [WindowInsets.ime]. Most apps will usually want to use the
+ * [Modifier.navigationBarsWithImePadding] modifier.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.imePadding(): Modifier = composed {
+ InsetsPaddingModifier(
+ insetsType = LocalWindowInsets.current.ime,
+ applyLeft = true,
+ applyRight = true,
+ applyBottom = true,
+ )
+}
+
+/**
+ * Apply additional space which matches the height of the [WindowInsets.ime] (on-screen keyboard)
+ * height and [WindowInsets.navigationBars]. This is what apps should use to handle any insets
+ * at the bottom of the screen.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsWithImePadding(): Modifier = composed {
+ InsetsPaddingModifier(
+ insetsType = LocalWindowInsets.current.ime,
+ minimumInsetsType = LocalWindowInsets.current.navigationBars,
+ applyLeft = true,
+ applyRight = true,
+ applyBottom = true,
+ )
+}
+
+private data class InsetsPaddingModifier(
+ private val insetsType: InsetsType,
+ private val minimumInsetsType: InsetsType? = null,
+ private val applyLeft: Boolean = false,
+ private val applyTop: Boolean = false,
+ private val applyRight: Boolean = false,
+ private val applyBottom: Boolean = false,
+) : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val transformedInsets = if (minimumInsetsType != null) {
+ // If we have a minimum insets, coerce each dimensions
+ insetsType.coerceEachDimensionAtLeast(minimumInsetsType)
+ } else insetsType
+
+ val left = if (applyLeft) transformedInsets.left else 0
+ val top = if (applyTop) transformedInsets.top else 0
+ val right = if (applyRight) transformedInsets.right else 0
+ val bottom = if (applyBottom) transformedInsets.bottom else 0
+ val horizontal = left + right
+ val vertical = top + bottom
+
+ val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+
+ val width = (placeable.width + horizontal)
+ .coerceIn(constraints.minWidth, constraints.maxWidth)
+ val height = (placeable.height + vertical)
+ .coerceIn(constraints.minHeight, constraints.maxHeight)
+ return layout(width, height) {
+ placeable.place(left, top)
+ }
+ }
+}
+
+/**
+ * Returns the current insets converted into a [PaddingValues].
+ *
+ * @param start Whether to apply the inset on the start dimension.
+ * @param top Whether to apply the inset on the top dimension.
+ * @param end Whether to apply the inset on the end dimension.
+ * @param bottom Whether to apply the inset on the bottom dimension.
+ * @param additionalHorizontal Value to add to the start and end dimensions.
+ * @param additionalVertical Value to add to the top and bottom dimensions.
+ */
+@Composable
+inline fun InsetsType.toPaddingValues(
+ start: Boolean = true,
+ top: Boolean = true,
+ end: Boolean = true,
+ bottom: Boolean = true,
+ additionalHorizontal: Dp = 0.dp,
+ additionalVertical: Dp = 0.dp,
+) = toPaddingValues(
+ start = start,
+ top = top,
+ end = end,
+ bottom = bottom,
+ additionalStart = additionalHorizontal,
+ additionalTop = additionalVertical,
+ additionalEnd = additionalHorizontal,
+ additionalBottom = additionalVertical
+)
+
+/**
+ * Returns the current insets converted into a [PaddingValues].
+ *
+ * @param start Whether to apply the inset on the start dimension.
+ * @param top Whether to apply the inset on the top dimension.
+ * @param end Whether to apply the inset on the end dimension.
+ * @param bottom Whether to apply the inset on the bottom dimension.
+ * @param additionalStart Value to add to the start dimension.
+ * @param additionalTop Value to add to the top dimension.
+ * @param additionalEnd Value to add to the end dimension.
+ * @param additionalBottom Value to add to the bottom dimension.
+ */
+@Composable
+fun InsetsType.toPaddingValues(
+ start: Boolean = true,
+ top: Boolean = true,
+ end: Boolean = true,
+ bottom: Boolean = true,
+ additionalStart: Dp = 0.dp,
+ additionalTop: Dp = 0.dp,
+ additionalEnd: Dp = 0.dp,
+ additionalBottom: Dp = 0.dp,
+): PaddingValues = with(LocalDensity.current) {
+ val layoutDirection = LocalLayoutDirection.current
+ PaddingValues(
+ start = additionalStart + when {
+ start && layoutDirection == LayoutDirection.Ltr -> [email protected]()
+ start && layoutDirection == LayoutDirection.Rtl -> [email protected]()
+ else -> 0.dp
+ },
+ top = additionalTop + when {
+ top -> [email protected]()
+ else -> 0.dp
+ },
+ end = additionalEnd + when {
+ end && layoutDirection == LayoutDirection.Ltr -> [email protected]()
+ end && layoutDirection == LayoutDirection.Rtl -> [email protected]()
+ else -> 0.dp
+ },
+ bottom = additionalBottom + when {
+ bottom -> [email protected]()
+ else -> 0.dp
+ }
+ )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt
new file mode 100644
index 0000000..115014d
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/insets/Size.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.
+ */
+
+/**
+ * TODO: Move to depending on Accompanist with prebuilts when we hit a stable version
+ * https://github.com/google/accompanist/blob/main/insets/src/main/java/com/google/accompanist
+ * /insets/Size.kt
+ */
+
+@file:Suppress("NOTHING_TO_INLINE", "unused")
+
+@file:JvmName("ComposeInsets")
+@file:JvmMultifileClass
+
+package androidx.compose.material.catalog.insets
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.height
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Declare the height of the content to match the height of the status bars exactly.
+ *
+ * This is very handy when used with `Spacer` to push content below the status bars:
+ * ```
+ * Column {
+ * Spacer(Modifier.statusBarHeight())
+ *
+ * // Content to be drawn below status bars (y-axis)
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the status bars:
+ * ```
+ * Spacer(
+ * Modifier.statusBarHeight()
+ * .fillMaxWidth()
+ * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param additional Any additional height to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.statusBarsHeight(
+ additional: Dp = 0.dp
+): Modifier = composed {
+ InsetsSizeModifier(
+ insetsType = LocalWindowInsets.current.statusBars,
+ heightSide = VerticalSide.Top,
+ additionalHeight = additional
+ )
+}
+
+/**
+ * Declare the preferred height of the content to match the height of the navigation bars when
+ * present at the bottom of the screen.
+ *
+ * This is very handy when used with `Spacer` to push content below the navigation bars:
+ * ```
+ * Column {
+ * // Content to be drawn above status bars (y-axis)
+ * Spacer(Modifier.navigationBarHeight())
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the navigation bars:
+ * ```
+ * Spacer(
+ * Modifier.navigationBarHeight()
+ * .fillMaxWidth()
+ * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param additional Any additional height to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsHeight(
+ additional: Dp = 0.dp
+): Modifier = composed {
+ InsetsSizeModifier(
+ insetsType = LocalWindowInsets.current.navigationBars,
+ heightSide = VerticalSide.Bottom,
+ additionalHeight = additional
+ )
+}
+
+/**
+ * Declare the preferred width of the content to match the width of the navigation bars,
+ * on the given [side].
+ *
+ * This is very handy when used with `Spacer` to push content inside from any vertical
+ * navigation bars (typically when the device is in landscape):
+ * ```
+ * Row {
+ * Spacer(Modifier.navigationBarWidth(HorizontalSide.Left))
+ *
+ * // Content to be inside the navigation bars (x-axis)
+ *
+ * Spacer(Modifier.navigationBarWidth(HorizontalSide.Right))
+ * }
+ * ```
+ *
+ * It's also useful when used to draw a scrim which matches the navigation bars:
+ * ```
+ * Spacer(
+ * Modifier.navigationBarWidth(HorizontalSide.Left)
+ * .fillMaxHeight()
+ * .drawBackground(MaterialTheme.colors.background.copy(alpha = 0.3f)
+ * )
+ * ```
+ *
+ * Internally this matches the behavior of the [Modifier.height] modifier.
+ *
+ * @param side The navigation bar side to use as the source for the width.
+ * @param additional Any additional width to add to the status bars size.
+ */
+@SuppressLint("ModifierInspectorInfo")
+fun Modifier.navigationBarsWidth(
+ side: HorizontalSide,
+ additional: Dp = 0.dp
+): Modifier = composed {
+ InsetsSizeModifier(
+ insetsType = LocalWindowInsets.current.navigationBars,
+ widthSide = side,
+ additionalWidth = additional
+ )
+}
+
+/**
+ * [Modifier] class which powers the modifiers above. This is the lower level modifier which
+ * supports the functionality through a number of parameters.
+ *
+ * We may make this public at some point. If you need this, please let us know via the
+ * issue tracker.
+ */
+private data class InsetsSizeModifier(
+ private val insetsType: InsetsType,
+ private val widthSide: HorizontalSide? = null,
+ private val additionalWidth: Dp = 0.dp,
+ private val heightSide: VerticalSide? = null,
+ private val additionalHeight: Dp = 0.dp
+) : LayoutModifier {
+ private val Density.targetConstraints: Constraints
+ get() {
+ val additionalWidthPx = additionalWidth.roundToPx()
+ val additionalHeightPx = additionalHeight.roundToPx()
+ return Constraints(
+ minWidth = additionalWidthPx + when (widthSide) {
+ HorizontalSide.Left -> insetsType.left
+ HorizontalSide.Right -> insetsType.right
+ null -> 0
+ },
+ minHeight = additionalHeightPx + when (heightSide) {
+ VerticalSide.Top -> insetsType.top
+ VerticalSide.Bottom -> insetsType.bottom
+ null -> 0
+ },
+ maxWidth = when (widthSide) {
+ HorizontalSide.Left -> insetsType.left + additionalWidthPx
+ HorizontalSide.Right -> insetsType.right + additionalWidthPx
+ null -> Constraints.Infinity
+ },
+ maxHeight = when (heightSide) {
+ VerticalSide.Top -> insetsType.top + additionalHeightPx
+ VerticalSide.Bottom -> insetsType.bottom + additionalHeightPx
+ null -> Constraints.Infinity
+ }
+ )
+ }
+
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val wrappedConstraints = targetConstraints.let { targetConstraints ->
+ val resolvedMinWidth = if (widthSide != null) {
+ targetConstraints.minWidth
+ } else {
+ constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
+ }
+ val resolvedMaxWidth = if (widthSide != null) {
+ targetConstraints.maxWidth
+ } else {
+ constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
+ }
+ val resolvedMinHeight = if (heightSide != null) {
+ targetConstraints.minHeight
+ } else {
+ constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
+ }
+ val resolvedMaxHeight = if (heightSide != null) {
+ targetConstraints.maxHeight
+ } else {
+ constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
+ }
+ Constraints(
+ resolvedMinWidth,
+ resolvedMaxWidth,
+ resolvedMinHeight,
+ resolvedMaxHeight
+ )
+ }
+ val placeable = measurable.measure(wrappedConstraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.place(0, 0)
+ }
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ) = measurable.minIntrinsicWidth(height).let {
+ val constraints = targetConstraints
+ it.coerceIn(constraints.minWidth, constraints.maxWidth)
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+ measurable: IntrinsicMeasurable,
+ height: Int
+ ) = measurable.maxIntrinsicWidth(height).let {
+ val constraints = targetConstraints
+ it.coerceIn(constraints.minWidth, constraints.maxWidth)
+ }
+
+ override fun IntrinsicMeasureScope.minIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ) = measurable.minIntrinsicHeight(width).let {
+ val constraints = targetConstraints
+ it.coerceIn(constraints.minHeight, constraints.maxHeight)
+ }
+
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+ measurable: IntrinsicMeasurable,
+ width: Int
+ ) = measurable.maxIntrinsicHeight(width).let {
+ val constraints = targetConstraints
+ it.coerceIn(constraints.minHeight, constraints.maxHeight)
+ }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt
new file mode 100644
index 0000000..5e17a3a
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Components.kt
@@ -0,0 +1,209 @@
+/*
+ * 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.material.catalog.model
+
+import androidx.annotation.DrawableRes
+import androidx.compose.material.catalog.R
+
+data class Component(
+ val id: Int,
+ val name: String,
+ val description: String,
+ @DrawableRes
+ val icon: Int = R.drawable.ic_component,
+ val examples: List<Example>
+)
+
+private val AppBarsBottom = Component(
+ id = 1,
+ name = "App bars: bottom",
+ description = "A bottom app bar displays navigation and key actions at the bottom of mobile " +
+ "screens.",
+ examples = AppBarsBottomExamples
+)
+
+private val AppBarsTop = Component(
+ id = 2,
+ name = "App bars: top",
+ description = "The top app bar displays information and actions relating to the current " +
+ "screen.",
+ examples = AppBarsTopExamples
+)
+
+private val Backdrop = Component(
+ id = 3,
+ name = "Backdrop",
+ description = "A backdrop appears behind all other surfaces in an app, displaying contextual " +
+ "and actionable content.",
+ examples = BackdropExamples
+)
+
+private val BottomNavigation = Component(
+ id = 4,
+ name = "Bottom navigation",
+ description = "Bottom navigation bars allow movement between primary destinations in an app.",
+ examples = BottomNavigationExamples
+)
+
+private val Buttons = Component(
+ id = 5,
+ name = "Buttons",
+ description = "Buttons allow users to take actions, and make choices, with a single tap.",
+ examples = ButtonsExamples
+)
+
+private val ButtonsFloatingActionButton = Component(
+ id = 6,
+ name = "Buttons: floating action button",
+ description = "A floating action button (FAB) represents the primary action of a screen.",
+ examples = ButtonsFloatingActionButtonExamples
+)
+
+private val Cards = Component(
+ id = 7,
+ name = "Cards",
+ description = "Cards contain content and actions about a single subject.",
+ examples = CardsExamples
+)
+
+private val Checkboxes = Component(
+ id = 8,
+ name = "Checkboxes",
+ description = "Checkboxes allow the user to select one or more items from a set or turn an " +
+ "option on or off.",
+ examples = CheckboxesExamples
+)
+
+private val Dialogs = Component(
+ id = 9,
+ name = "Dialogs",
+ description = "Dialogs inform users about a task and can contain critical information, " +
+ "require decisions, or involve multiple tasks.",
+ examples = DialogsExamples
+)
+
+private val Dividers = Component(
+ id = 10,
+ name = "Dividers",
+ description = "A divider is a thin line that groups content in lists and layouts.",
+ examples = DividersExamples
+)
+
+private val Lists = Component(
+ id = 11,
+ name = "Lists",
+ description = "Lists are continuous, vertical indexes of text or images.",
+ examples = ListsExamples
+)
+
+private val Menus = Component(
+ id = 12,
+ name = "Menus",
+ description = "Menus display a list of choices on temporary surfaces.",
+ examples = MenusExamples
+)
+
+private val NavigationDrawer = Component(
+ id = 13,
+ name = "Navigation drawer",
+ description = "Navigation drawers provide access to destinations in your app.",
+ examples = NavigationDrawerExamples
+)
+
+private val ProgressIndicators = Component(
+ id = 14,
+ name = "Progress indicators",
+ description = "Progress indicators express an unspecified wait time or display the length of " +
+ "a process.",
+ examples = ProgressIndicatorsExamples
+)
+
+private val RadioButtons = Component(
+ id = 15,
+ name = "Radio buttons",
+ description = "Radio buttons allow the user to select one option from a set.",
+ examples = RadioButtonsExamples
+)
+
+private val SheetsBottom = Component(
+ id = 16,
+ name = "Sheets: bottom",
+ description = "Bottom sheets are surfaces containing supplementary content that are anchored " +
+ "to the bottom of the screen.",
+ examples = SheetsBottomExamples
+)
+
+private val Sliders = Component(
+ id = 17,
+ name = "Sliders",
+ description = "Sliders allow users to make selections from a range of values.",
+ examples = SlidersExamples
+)
+
+private val Snackbars = Component(
+ id = 18,
+ name = "Snackbars",
+ description = "Snackbars provide brief messages about app processes at the bottom of the " +
+ "screen.",
+ examples = SnackbarsExamples
+)
+
+private val Switches = Component(
+ id = 19,
+ name = "Switches",
+ description = "Switches toggle the state of a single setting on or off.",
+ examples = SwitchesExamples
+)
+
+private val Tabs = Component(
+ id = 20,
+ name = "Tabs",
+ description = "Tabs organize content across different screens, data sets, and other " +
+ "interactions.",
+ examples = TabsExamples
+)
+
+private val TextFields = Component(
+ id = 21,
+ name = "Text fields",
+ description = "Text fields let users enter and edit text.",
+ examples = TextFieldsExamples
+)
+
+val Components = listOf(
+ AppBarsBottom,
+ AppBarsTop,
+ Backdrop,
+ BottomNavigation,
+ Buttons,
+ ButtonsFloatingActionButton,
+ Cards,
+ Checkboxes,
+ Dialogs,
+ Dividers,
+ Lists,
+ Menus,
+ NavigationDrawer,
+ ProgressIndicators,
+ RadioButtons,
+ SheetsBottom,
+ Sliders,
+ Snackbars,
+ Switches,
+ Tabs,
+ TextFields
+)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt
new file mode 100644
index 0000000..8a192f8
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/model/Examples.kt
@@ -0,0 +1,524 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("COMPOSABLE_FUNCTION_REFERENCE")
+
+package androidx.compose.material.catalog.model
+
+import androidx.compose.material.samples.AlertDialogSample
+import androidx.compose.material.samples.BackdropScaffoldSample
+import androidx.compose.material.samples.BottomDrawerSample
+import androidx.compose.material.samples.BottomNavigationSample
+import androidx.compose.material.samples.BottomNavigationWithOnlySelectedLabelsSample
+import androidx.compose.material.samples.BottomSheetScaffoldSample
+import androidx.compose.material.samples.ButtonSample
+import androidx.compose.material.samples.ButtonWithIconSample
+import androidx.compose.material.samples.CardSample
+import androidx.compose.material.samples.CheckboxSample
+import androidx.compose.material.samples.CircularProgressIndicatorSample
+import androidx.compose.material.samples.ClickableListItems
+import androidx.compose.material.samples.CustomAlertDialogSample
+import androidx.compose.material.samples.FancyIndicatorContainerTabs
+import androidx.compose.material.samples.FancyIndicatorTabs
+import androidx.compose.material.samples.FancyTabs
+import androidx.compose.material.samples.FluidExtendedFab
+import androidx.compose.material.samples.IconTabs
+import androidx.compose.material.samples.LeadingIconTabs
+import androidx.compose.material.samples.LinearProgressIndicatorSample
+import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.ModalBottomSheetSample
+import androidx.compose.material.samples.ModalDrawerSample
+import androidx.compose.material.samples.OneLineListItems
+import androidx.compose.material.samples.OneLineRtlLtrListItems
+import androidx.compose.material.samples.OutlinedButtonSample
+import androidx.compose.material.samples.OutlinedTextFieldSample
+import androidx.compose.material.samples.PasswordTextField
+import androidx.compose.material.samples.RadioButtonSample
+import androidx.compose.material.samples.RadioGroupSample
+import androidx.compose.material.samples.ScaffoldWithCoroutinesSnackbar
+import androidx.compose.material.samples.ScaffoldWithCustomSnackbar
+import androidx.compose.material.samples.ScaffoldWithSimpleSnackbar
+import androidx.compose.material.samples.ScrollingFancyIndicatorContainerTabs
+import androidx.compose.material.samples.ScrollingTextTabs
+import androidx.compose.material.samples.SimpleBottomAppBar
+import androidx.compose.material.samples.SimpleExtendedFabNoIcon
+import androidx.compose.material.samples.SimpleExtendedFabWithIcon
+import androidx.compose.material.samples.SimpleFab
+import androidx.compose.material.samples.SimpleOutlinedTextFieldSample
+import androidx.compose.material.samples.SimpleTextFieldSample
+import androidx.compose.material.samples.SimpleTopAppBar
+import androidx.compose.material.samples.SliderSample
+import androidx.compose.material.samples.StepsSliderSample
+import androidx.compose.material.samples.SwitchSample
+import androidx.compose.material.samples.TextAndIconTabs
+import androidx.compose.material.samples.TextButtonSample
+import androidx.compose.material.samples.TextFieldSample
+import androidx.compose.material.samples.TextFieldWithErrorState
+import androidx.compose.material.samples.TextFieldWithHelperMessage
+import androidx.compose.material.samples.TextFieldWithHideKeyboardOnImeAction
+import androidx.compose.material.samples.TextFieldWithIcons
+import androidx.compose.material.samples.TextFieldWithPlaceholder
+import androidx.compose.material.samples.TextTabs
+import androidx.compose.material.samples.ThreeLineListItems
+import androidx.compose.material.samples.ThreeLineRtlLtrListItems
+import androidx.compose.material.samples.TriStateCheckboxSample
+import androidx.compose.material.samples.TwoLineListItems
+import androidx.compose.material.samples.TwoLineRtlLtrListItems
+import androidx.compose.runtime.Composable
+
+data class Example(
+ val name: String,
+ val description: String,
+ val content: @Composable () -> Unit
+)
+
+private const val AppBarsBottomExampleDescription = "App bars: bottom examples"
+val AppBarsBottomExamples = listOf(
+ Example(
+ name = ::SimpleBottomAppBar.name,
+ description = AppBarsBottomExampleDescription
+ ) {
+ SimpleBottomAppBar()
+ }
+)
+
+private const val AppBarsTopExampleDescription = "App bars: top examples"
+val AppBarsTopExamples = listOf(
+ Example(
+ name = ::SimpleTopAppBar.name,
+ description = AppBarsTopExampleDescription
+ ) {
+ SimpleTopAppBar()
+ }
+)
+
+private const val BackdropExampleDescription = "Backdrop examples"
+val BackdropExamples = listOf(
+ Example(
+ name = ::BackdropScaffoldSample.name,
+ description = BackdropExampleDescription
+ ) {
+ BackdropScaffoldSample()
+ }
+)
+
+private const val BottomNavigationExampleDescription = "Bottom navigation examples"
+val BottomNavigationExamples = listOf(
+ Example(
+ name = ::BottomNavigationSample.name,
+ description = BottomNavigationExampleDescription
+ ) {
+ BottomNavigationSample()
+ },
+ Example(
+ name = ::BottomNavigationWithOnlySelectedLabelsSample.name,
+ description = BottomNavigationExampleDescription
+ ) {
+ BottomNavigationWithOnlySelectedLabelsSample()
+ }
+)
+
+private const val ButtonsExampleDescription = "Buttons examples"
+val ButtonsExamples = listOf(
+ Example(
+ name = ::ButtonSample.name,
+ description = ButtonsExampleDescription
+ ) {
+ ButtonSample()
+ },
+ Example(
+ name = ::OutlinedButtonSample.name,
+ description = ButtonsExampleDescription
+ ) {
+ OutlinedButtonSample()
+ },
+ Example(
+ name = ::TextButtonSample.name,
+ description = ButtonsExampleDescription
+ ) {
+ TextButtonSample()
+ },
+ Example(
+ name = ::ButtonWithIconSample.name,
+ description = ButtonsExampleDescription
+ ) {
+ ButtonWithIconSample()
+ }
+)
+
+private const val ButtonsFloatingActionButtonExampleDescription = "Buttons: floating action " +
+ "button examples"
+val ButtonsFloatingActionButtonExamples = listOf(
+ Example(
+ name = ::SimpleFab.name,
+ description = ButtonsFloatingActionButtonExampleDescription
+ ) {
+ SimpleFab()
+ },
+ Example(
+ name = ::SimpleExtendedFabNoIcon.name,
+ description = ButtonsFloatingActionButtonExampleDescription
+ ) {
+ SimpleExtendedFabNoIcon()
+ },
+ Example(
+ name = ::SimpleExtendedFabWithIcon.name,
+ description = ButtonsFloatingActionButtonExampleDescription
+ ) {
+ SimpleExtendedFabWithIcon()
+ },
+ Example(
+ name = ::FluidExtendedFab.name,
+ description = ButtonsFloatingActionButtonExampleDescription
+ ) {
+ FluidExtendedFab()
+ },
+)
+
+private const val CardsExampleDescription = "Cards examples"
+val CardsExamples = listOf(
+ Example(
+ name = ::CardSample.name,
+ description = CardsExampleDescription
+ ) {
+ CardSample()
+ }
+)
+
+private const val CheckboxesExampleDescription = "Checkboxes examples"
+val CheckboxesExamples = listOf(
+ Example(
+ name = ::CheckboxSample.name,
+ description = CheckboxesExampleDescription
+ ) {
+ CheckboxSample()
+ },
+ Example(
+ name = ::TriStateCheckboxSample.name,
+ description = CheckboxesExampleDescription
+ ) {
+ TriStateCheckboxSample()
+ }
+)
+
+private const val DialogsExampleDescription = "Dialogs examples"
+val DialogsExamples = listOf(
+ Example(
+ name = ::AlertDialogSample.name,
+ description = DialogsExampleDescription
+ ) {
+ AlertDialogSample()
+ },
+ Example(
+ name = ::CustomAlertDialogSample.name,
+ description = DialogsExampleDescription
+ ) {
+ CustomAlertDialogSample()
+ }
+)
+
+// No divider samples
+val DividersExamples = emptyList<Example>()
+
+private const val ListsExampleDescription = "Lists examples"
+val ListsExamples = listOf(
+ Example(
+ name = ::ClickableListItems.name,
+ description = ListsExampleDescription
+ ) {
+ ClickableListItems()
+ },
+ Example(
+ name = ::OneLineListItems.name,
+ description = ListsExampleDescription
+ ) {
+ OneLineListItems()
+ },
+ Example(
+ name = ::TwoLineListItems.name,
+ description = ListsExampleDescription
+ ) {
+ TwoLineListItems()
+ },
+ Example(
+ name = ::ThreeLineListItems.name,
+ description = ListsExampleDescription
+ ) {
+ ThreeLineListItems()
+ },
+ Example(
+ name = ::OneLineRtlLtrListItems.name,
+ description = ListsExampleDescription
+ ) {
+ OneLineRtlLtrListItems()
+ },
+ Example(
+ name = ::TwoLineRtlLtrListItems.name,
+ description = ListsExampleDescription
+ ) {
+ TwoLineRtlLtrListItems()
+ },
+ Example(
+ name = ::ThreeLineRtlLtrListItems.name,
+ description = ListsExampleDescription
+ ) {
+ ThreeLineRtlLtrListItems()
+ }
+)
+
+private const val MenusExampleDescription = "Menus examples"
+val MenusExamples = listOf(
+ Example(
+ name = ::MenuSample.name,
+ description = MenusExampleDescription
+ ) {
+ MenuSample()
+ }
+)
+
+private const val NavigationDrawerExampleDescription = "Navigation drawer examples"
+val NavigationDrawerExamples = listOf(
+ Example(
+ name = ::ModalDrawerSample.name,
+ description = NavigationDrawerExampleDescription
+ ) {
+ ModalDrawerSample()
+ },
+ Example(
+ name = ::BottomDrawerSample.name,
+ description = NavigationDrawerExampleDescription
+ ) {
+ BottomDrawerSample()
+ }
+)
+
+private const val ProgressIndicatorsExampleDescription = "Progress indicators examples"
+val ProgressIndicatorsExamples = listOf(
+ Example(
+ name = ::LinearProgressIndicatorSample.name,
+ description = ProgressIndicatorsExampleDescription
+ ) {
+ LinearProgressIndicatorSample()
+ },
+ Example(
+ name = ::CircularProgressIndicatorSample.name,
+ description = ProgressIndicatorsExampleDescription
+ ) {
+ CircularProgressIndicatorSample()
+ }
+)
+
+private const val RadioButtonsExampleDescription = "Radio buttons examples"
+val RadioButtonsExamples = listOf(
+ Example(
+ name = ::RadioButtonSample.name,
+ description = RadioButtonsExampleDescription
+ ) {
+ RadioButtonSample()
+ },
+ Example(
+ name = ::RadioGroupSample.name,
+ description = RadioButtonsExampleDescription
+ ) {
+ RadioGroupSample()
+ },
+)
+
+private const val SheetsBottomExampleDescription = "Sheets: bottom examples"
+val SheetsBottomExamples = listOf(
+ Example(
+ name = ::BottomSheetScaffoldSample.name,
+ description = SheetsBottomExampleDescription
+ ) {
+ BottomSheetScaffoldSample()
+ },
+ Example(
+ name = ::ModalBottomSheetSample.name,
+ description = SheetsBottomExampleDescription
+ ) {
+ ModalBottomSheetSample()
+ }
+)
+
+private const val SlidersExampleDescription = "Sliders examples"
+val SlidersExamples = listOf(
+ Example(
+ name = ::SliderSample.name,
+ description = SlidersExampleDescription
+ ) {
+ SliderSample()
+ },
+ Example(
+ name = ::StepsSliderSample.name,
+ description = SlidersExampleDescription
+ ) {
+ StepsSliderSample()
+ }
+)
+
+private const val SnackbarsExampleDescription = "Snackbars examples"
+val SnackbarsExamples = listOf(
+ Example(
+ name = ::ScaffoldWithSimpleSnackbar.name,
+ description = SnackbarsExampleDescription
+ ) {
+ ScaffoldWithSimpleSnackbar()
+ },
+ Example(
+ name = ::ScaffoldWithCustomSnackbar.name,
+ description = SnackbarsExampleDescription
+ ) {
+ ScaffoldWithCustomSnackbar()
+ },
+ Example(
+ name = ::ScaffoldWithCoroutinesSnackbar.name,
+ description = SnackbarsExampleDescription
+ ) {
+ ScaffoldWithCoroutinesSnackbar()
+ }
+)
+
+private const val SwitchesExampleDescription = "Switches examples"
+val SwitchesExamples = listOf(
+ Example(
+ name = ::SwitchSample.name,
+ description = SwitchesExampleDescription
+ ) {
+ SwitchSample()
+ }
+)
+
+private const val TabsExampleDescription = "Tabs examples"
+val TabsExamples = listOf(
+ Example(
+ name = ::TextTabs.name,
+ description = TabsExampleDescription
+ ) {
+ TextTabs()
+ },
+ Example(
+ name = ::IconTabs.name,
+ description = TabsExampleDescription
+ ) {
+ IconTabs()
+ },
+ Example(
+ name = ::TextAndIconTabs.name,
+ description = TabsExampleDescription
+ ) {
+ TextAndIconTabs()
+ },
+ Example(
+ name = ::LeadingIconTabs.name,
+ description = TabsExampleDescription
+ ) {
+ LeadingIconTabs()
+ },
+ Example(
+ name = ::ScrollingTextTabs.name,
+ description = TabsExampleDescription
+ ) {
+ ScrollingTextTabs()
+ },
+ Example(
+ name = ::FancyTabs.name,
+ description = TabsExampleDescription
+ ) {
+ FancyTabs()
+ },
+ Example(
+ name = ::FancyIndicatorTabs.name,
+ description = TabsExampleDescription
+ ) {
+ FancyIndicatorTabs()
+ },
+ Example(
+ name = ::FancyIndicatorContainerTabs.name,
+ description = TabsExampleDescription
+ ) {
+ FancyIndicatorContainerTabs()
+ },
+ Example(
+ name = ::ScrollingFancyIndicatorContainerTabs.name,
+ description = TabsExampleDescription
+ ) {
+ ScrollingFancyIndicatorContainerTabs()
+ }
+)
+
+private const val TextFieldsExampleDescription = "Text fields examples"
+val TextFieldsExamples = listOf(
+ Example(
+ name = ::SimpleTextFieldSample.name,
+ description = TextFieldsExampleDescription
+ ) {
+ SimpleTextFieldSample()
+ },
+ Example(
+ name = ::TextFieldSample.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldSample()
+ },
+ Example(
+ name = ::SimpleOutlinedTextFieldSample.name,
+ description = TextFieldsExampleDescription
+ ) {
+ SimpleOutlinedTextFieldSample()
+ },
+ Example(
+ name = ::OutlinedTextFieldSample.name,
+ description = TextFieldsExampleDescription
+ ) {
+ OutlinedTextFieldSample()
+ },
+ Example(
+ name = ::TextFieldWithIcons.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldWithIcons()
+ },
+ Example(
+ name = ::TextFieldWithPlaceholder.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldWithPlaceholder()
+ },
+ Example(
+ name = ::TextFieldWithErrorState.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldWithErrorState()
+ },
+ Example(
+ name = ::TextFieldWithHelperMessage.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldWithHelperMessage()
+ },
+ Example(
+ name = ::PasswordTextField.name,
+ description = TextFieldsExampleDescription
+ ) {
+ PasswordTextField()
+ },
+ Example(
+ name = ::TextFieldWithHideKeyboardOnImeAction.name,
+ description = TextFieldsExampleDescription
+ ) {
+ TextFieldWithHideKeyboardOnImeAction()
+ }
+)
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt
new file mode 100644
index 0000000..9d0fc08
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/Border.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.material.catalog.ui.common
+
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Draws a stroke border on the inner edges of grid items i.e. bottom as well as end (if not the
+ * last item in a row).
+ *
+ * @param itemIndex The zero-based index of the grid item.
+ * @param cellsCount The number of cells (columns for vertical, rows for horizontal) in the grid.
+ * @param color The color of the border.
+ * @param width The width of the border.
+ */
+fun Modifier.gridItemBorder(
+ itemIndex: Int,
+ cellsCount: Int,
+ color: Color,
+ width: Dp = BorderWidth
+) = drawBehind {
+ val end = itemIndex.inc().rem(cellsCount) == 0
+ drawLine(
+ color = color,
+ strokeWidth = width.toPx(),
+ cap = StrokeCap.Square,
+ start = Offset(0f, size.height),
+ end = Offset(size.width, size.height)
+ )
+ if (!end) drawLine(
+ color = color,
+ strokeWidth = width.toPx(),
+ cap = StrokeCap.Square,
+ start = Offset(size.width, size.height),
+ end = Offset(size.width, 0f)
+ )
+}
+
+/**
+ * Composite of local content color at 12% alpha over background color, used by borders.
+ */
+@Composable
+fun compositeBorderColor(): Color = LocalContentColor.current.copy(alpha = BorderAlpha)
+ .compositeOver(MaterialTheme.colors.background)
+
+val BorderWidth = 1.dp
+private const val BorderAlpha = 0.12f
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt
new file mode 100644
index 0000000..0cb6307
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogScaffold.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.material.catalog.ui.common
+
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.material.Scaffold
+import androidx.compose.material.catalog.util.DOCS_URL
+import androidx.compose.material.catalog.util.GUIDELINES_URL
+import androidx.compose.material.catalog.util.ISSUE_URL
+import androidx.compose.material.catalog.util.SOURCE_URL
+import androidx.compose.material.catalog.util.openUrl
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+
+@Composable
+fun CatalogScaffold(
+ topBarTitle: String,
+ showBackNavigationIcon: Boolean = false,
+ onBackClick: () -> Unit = {},
+ content: @Composable (PaddingValues) -> Unit
+) {
+ val context = LocalContext.current
+ Scaffold(
+ topBar = {
+ CatalogTopAppBar(
+ title = topBarTitle,
+ showBackNavigationIcon = showBackNavigationIcon,
+ onBackClick = onBackClick,
+ onGuidelinesClick = { context.openUrl(GUIDELINES_URL) },
+ onDocsClick = { context.openUrl(DOCS_URL) },
+ onSourceClick = { context.openUrl(SOURCE_URL) },
+ onIssueClick = { context.openUrl(ISSUE_URL) }
+ )
+ },
+ content = content
+ )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt
new file mode 100644
index 0000000..5b3dba8
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/common/CatalogTopAppBar.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.material.catalog.ui.common
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.material.AppBarDefaults
+import androidx.compose.material.Divider
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.navigationBarsPadding
+import androidx.compose.material.catalog.insets.statusBarsPadding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ArrowBack
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.primarySurface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun CatalogTopAppBar(
+ title: String,
+ showBackNavigationIcon: Boolean = false,
+ onBackClick: () -> Unit = {},
+ onThemeClick: () -> Unit = {},
+ onGuidelinesClick: () -> Unit = {},
+ onDocsClick: () -> Unit = {},
+ onSourceClick: () -> Unit = {},
+ onIssueClick: () -> Unit = {},
+) {
+ var moreMenuExpanded by remember { mutableStateOf(false) }
+ // Wrapping in a Surface to handle window insets
+ // https://issuetracker.google.com/issues/183161866
+ Surface(
+ color = MaterialTheme.colors.primarySurface,
+ elevation = AppBarDefaults.TopAppBarElevation
+ ) {
+ TopAppBar(
+ title = {
+ Text(
+ text = title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ actions = {
+ Box {
+ Row {
+ IconButton(onClick = onThemeClick) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_palette_24dp),
+ contentDescription = null
+ )
+ }
+ IconButton(onClick = { moreMenuExpanded = true }) {
+ Icon(
+ imageVector = Icons.Default.MoreVert,
+ contentDescription = null
+ )
+ }
+ }
+ MoreMenu(
+ expanded = moreMenuExpanded,
+ onDismissRequest = { moreMenuExpanded = false },
+ onGuidelinesClick = {
+ onGuidelinesClick()
+ moreMenuExpanded = false
+ },
+ onDocsClick = {
+ onDocsClick()
+ moreMenuExpanded = false
+ },
+ onSourceClick = {
+ onSourceClick()
+ moreMenuExpanded = false
+ },
+ onIssueClick = {
+ onIssueClick()
+ moreMenuExpanded = false
+ }
+ )
+ }
+ },
+ navigationIcon = if (showBackNavigationIcon) {
+ {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.Default.ArrowBack,
+ contentDescription = null
+ )
+ }
+ }
+ } else {
+ null
+ },
+ backgroundColor = Color.Transparent,
+ elevation = 0.dp,
+ modifier = Modifier
+ .statusBarsPadding()
+ .navigationBarsPadding(bottom = false)
+ )
+ }
+}
+
+@Composable
+private fun MoreMenu(
+ expanded: Boolean,
+ onDismissRequest: () -> Unit,
+ onGuidelinesClick: () -> Unit,
+ onDocsClick: () -> Unit,
+ onSourceClick: () -> Unit,
+ onIssueClick: () -> Unit
+) {
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = onDismissRequest
+ ) {
+ DropdownMenuItem(onClick = onGuidelinesClick) {
+ Text(stringResource(id = R.string.view_design_guidelines))
+ }
+ DropdownMenuItem(onClick = onDocsClick) {
+ Text(stringResource(id = R.string.view_developer_docs))
+ }
+ DropdownMenuItem(onClick = onSourceClick) {
+ Text(stringResource(id = R.string.view_source_code))
+ }
+ Divider()
+ DropdownMenuItem(onClick = onIssueClick) {
+ Text(stringResource(id = R.string.report_an_issue))
+ }
+ }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt
new file mode 100644
index 0000000..86a453b
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/Component.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.material.catalog.ui.component
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.LocalWindowInsets
+import androidx.compose.material.catalog.insets.toPaddingValues
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.material.catalog.ui.example.ExampleItem
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun Component(
+ component: Component,
+ onExampleClick: (example: Example) -> Unit,
+ onBackClick: () -> Unit
+) {
+ CatalogScaffold(
+ topBarTitle = component.name,
+ showBackNavigationIcon = true,
+ onBackClick = onBackClick
+ ) { paddingValues ->
+ LazyColumn(
+ modifier = Modifier
+ .padding(paddingValues)
+ .padding(horizontal = ComponentPadding),
+ contentPadding = LocalWindowInsets.current.navigationBars.toPaddingValues()
+ ) {
+ item {
+ Icon(
+ painter = painterResource(id = component.icon),
+ contentDescription = null,
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(ComponentIconHeight)
+ .padding(vertical = ComponentIconVerticalPadding),
+ // TODO: Remove tint when using component tile images
+ tint = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
+ )
+ }
+ item {
+ Text(
+ text = stringResource(id = R.string.description),
+ style = MaterialTheme.typography.body1
+ )
+ Spacer(modifier = Modifier.height(ComponentPadding))
+ Text(
+ text = component.description,
+ style = MaterialTheme.typography.body2
+ )
+ Spacer(modifier = Modifier.height(ComponentDescriptionPadding))
+ }
+ item {
+ Text(
+ text = stringResource(id = R.string.examples),
+ style = MaterialTheme.typography.body1
+ )
+ Spacer(modifier = Modifier.height(ComponentPadding))
+ }
+ if (component.examples.isNotEmpty()) {
+ items(component.examples) { example ->
+ ExampleItem(
+ example = example,
+ onClick = onExampleClick
+ )
+ Spacer(modifier = Modifier.height(ComponentPadding))
+ }
+ } else {
+ item {
+ Text(
+ text = stringResource(id = R.string.no_examples),
+ style = MaterialTheme.typography.body2
+ )
+ Spacer(modifier = Modifier.height(ComponentPadding))
+ }
+ }
+ }
+ }
+}
+
+private val ComponentIconHeight = 192.dp
+private val ComponentIconVerticalPadding = 42.dp
+private val ComponentPadding = 16.dp
+private val ComponentDescriptionPadding = 32.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt
new file mode 100644
index 0000000..8d55544
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/component/ComponentItem.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.material.catalog.ui.component
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.ui.common.compositeBorderColor
+import androidx.compose.material.catalog.ui.common.gridItemBorder
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ComponentItem(
+ component: Component,
+ onClick: (component: Component) -> Unit,
+ index: Int,
+ cellsCount: Int,
+) {
+ Box(
+ modifier = Modifier
+ .height(ComponentItemHeight)
+ .clickable { onClick(component) }
+ .gridItemBorder(
+ itemIndex = index,
+ cellsCount = cellsCount,
+ color = compositeBorderColor()
+ )
+ .padding(ComponentItemPadding)
+ ) {
+ Icon(
+ painter = painterResource(id = component.icon),
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.Center),
+ // TODO: Remove tint when using component tile images
+ tint = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
+ )
+ Text(
+ text = component.name,
+ modifier = Modifier.align(Alignment.BottomStart),
+ style = MaterialTheme.typography.caption
+ )
+ }
+}
+
+private val ComponentItemHeight = 164.dp
+private val ComponentItemPadding = 16.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt
new file mode 100644
index 0000000..245bc7e
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/Example.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.material.catalog.ui.example
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.catalog.insets.navigationBarsPadding
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+
+@Composable
+fun Example(
+ example: Example,
+ onBackClick: () -> Unit
+) {
+ CatalogScaffold(
+ topBarTitle = example.name,
+ showBackNavigationIcon = true,
+ onBackClick = onBackClick
+ ) { paddingValues ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(paddingValues)
+ .navigationBarsPadding(),
+ contentAlignment = Alignment.Center
+ ) {
+ example.content()
+ }
+ }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt
new file mode 100644
index 0000000..874afa1
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/example/ExampleItem.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.material.catalog.ui.example
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Card
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.Icon
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.catalog.model.Example
+import androidx.compose.material.catalog.ui.common.BorderWidth
+import androidx.compose.material.catalog.ui.common.compositeBorderColor
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowRight
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ExampleItem(
+ example: Example,
+ onClick: (example: Example) -> Unit
+) {
+ Card(
+ elevation = 0.dp,
+ border = BorderStroke(
+ width = BorderWidth,
+ color = compositeBorderColor()
+ ),
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(
+ modifier = Modifier
+ .clickable { onClick(example) }
+ .padding(ExampleItemPadding)
+ ) {
+ Column(modifier = Modifier.weight(1f, fill = true)) {
+ Text(
+ text = example.name,
+ style = MaterialTheme.typography.subtitle2
+ )
+ Spacer(modifier = Modifier.height(ExampleItemTextPadding))
+ CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
+ Text(
+ text = example.description,
+ style = MaterialTheme.typography.caption
+ )
+ }
+ }
+ Spacer(modifier = Modifier.width(ExampleItemPadding))
+ Icon(
+ imageVector = Icons.Default.KeyboardArrowRight,
+ contentDescription = null,
+ modifier = Modifier.align(Alignment.CenterVertically)
+ )
+ }
+ }
+}
+
+private val ExampleItemPadding = 16.dp
+private val ExampleItemTextPadding = 8.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt
new file mode 100644
index 0000000..ad30d01
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/home/Home.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.material.catalog.ui.home
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.GridCells
+import androidx.compose.foundation.lazy.LazyVerticalGrid
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.catalog.R
+import androidx.compose.material.catalog.insets.LocalWindowInsets
+import androidx.compose.material.catalog.insets.toPaddingValues
+import androidx.compose.material.catalog.model.Component
+import androidx.compose.material.catalog.ui.common.CatalogScaffold
+import androidx.compose.material.catalog.ui.component.ComponentItem
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+
+@Composable
+@OptIn(ExperimentalFoundationApi::class)
+fun Home(
+ components: List<Component>,
+ onComponentClick: (component: Component) -> Unit
+) {
+ CatalogScaffold(
+ topBarTitle = stringResource(id = R.string.material_components)
+ ) { paddingValues ->
+ BoxWithConstraints(modifier = Modifier.padding(paddingValues)) {
+ val cellsCount = maxOf((maxWidth / HomeCellMinSize).toInt(), 1)
+ LazyVerticalGrid(
+ // LazyGridScope doesn't expose nColumns from LazyVerticalGrid
+ // https://issuetracker.google.com/issues/183187002
+ cells = GridCells.Fixed(count = cellsCount),
+ content = {
+ itemsIndexed(components) { index, component ->
+ ComponentItem(
+ component = component,
+ onClick = onComponentClick,
+ index = index,
+ cellsCount = cellsCount
+ )
+ }
+ },
+ contentPadding = LocalWindowInsets.current.navigationBars.toPaddingValues()
+ )
+ }
+ }
+}
+
+private val HomeCellMinSize = 180.dp
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt
new file mode 100644
index 0000000..5432380
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/ui/theme/Theme.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.material.catalog.ui.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+
+@Composable
+fun CatalogTheme(content: @Composable () -> Unit) {
+ MaterialTheme(
+ colors = if (isSystemInDarkTheme()) {
+ darkColors()
+ } else {
+ lightColors()
+ },
+ content = content
+ )
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt
new file mode 100644
index 0000000..6bd63ad
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/util/Url.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.material.catalog.util
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+
+fun Context.openUrl(url: String) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ startActivity(intent)
+}
+
+const val GUIDELINES_URL = "https://material.io/components"
+const val DOCS_URL = "https://developer.android.com/jetpack/androidx/releases/compose-material"
+const val SOURCE_URL = "https://cs.android.com/androidx/platform/frameworks/support/+/" +
+ "androidx-main:compose/material/"
+const val ISSUE_URL = "https://issuetracker.google.com/issues/new?component=742043"
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml
new file mode 100644
index 0000000..84894df
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_component.xml
@@ -0,0 +1,35 @@
+<?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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="52dp"
+ android:height="53dp"
+ android:viewportWidth="52"
+ android:viewportHeight="53">
+ <!-- Long path to avoid Icon stroke line join bevel bug -->
+ <!-- https://issuetracker.google.com/issues/182794035 -->
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M32,0L32.0002,16.0599C32.4949,16.0202 32.9951,16 33.5,16C43.7173,16
+ 52,24.2827 52,34.5C52,44.7173 43.7173,53 33.5,53C23.2827,53 15,44.7173 15,34.5C15,33.6522
+ 15.057,32.8178 15.1675,32.0002L0,32L0,0L32,0ZM33.5,18.5C32.9944,18.5 32.4944,18.5234
+ 32.0008,18.5693L32,32L17.6941,32.0004C17.5663,32.8149 17.5,33.6497 17.5,34.5C17.5,43.3366
+ 24.6634,50.5 33.5,50.5C42.3366,50.5 49.5,43.3366 49.5,34.5C49.5,25.6634 42.3366,18.5
+ 33.5,18.5ZM29.5002,2.5L2.5,2.5L2.5,29.5L15.6835,29.5003C17.5115,22.9726 22.8306,17.9035
+ 29.5002,16.4335L29.5002,2.5ZM18.2968,29.5L29.5,29.5L29.5007,19.0039C24.2157,20.3639
+ 19.9874,24.3562 18.2968,29.5Z" />
+</vector>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml
new file mode 100644
index 0000000..b9e640b
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/drawable/ic_palette_24dp.xml
@@ -0,0 +1,42 @@
+<?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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12,22C6.49,22 2,17.51 2,12S6.49,2 12,2s10,4.04 10,9c0,3.31 -2.69,6
+ -6,6h-1.77c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.12 0.05,0.23 0.13,0.33 0.41,0.47 0.64,1.06
+ 0.64,1.67 0,1.38 -1.12,2.5 -2.5,2.5zM12,4c-4.41,0 -8,3.59 -8,8s3.59,8 8,8c0.28,0 0.5,-0.22
+ 0.5,-0.5 0,-0.16 -0.08,-0.28 -0.14,-0.35 -0.41,-0.46 -0.63,-1.05 -0.63,-1.65 0,-1.38
+ 1.12,-2.5 2.5,-2.5L16,15c2.21,0 4,-1.79 4,-4 0,-3.86 -3.59,-7 -8,-7z" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M6.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M9.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M14.5,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M17.5,11.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0" />
+</vector>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..62234c3
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values-night/themes.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="Theme.Catalog" parent="@android:style/Theme.Material.NoActionBar">
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">false</item>
+ </style>
+
+</resources>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml
new file mode 100644
index 0000000..6db56d476
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values/strings.xml
@@ -0,0 +1,33 @@
+<?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.
+ -->
+
+<resources>
+
+ <string name="app_name">Compose Material catalog</string>
+
+ <string name="material_components">Material Components</string>
+
+ <string name="description">Description</string>
+ <string name="examples">Examples</string>
+ <string name="no_examples">No examples</string>
+
+ <string name="view_design_guidelines">View design guidelines</string>
+ <string name="view_developer_docs">View developer docs</string>
+ <string name="view_source_code">View source code</string>
+ <string name="report_an_issue">Report an issue</string>
+
+</resources>
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml b/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml
new file mode 100644
index 0000000..4661c15
--- /dev/null
+++ b/compose/material/material/integration-tests/material-catalog/src/main/res/values/themes.xml
@@ -0,0 +1,26 @@
+<?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.
+ -->
+
+<resources xmlns:tools="http://schemas.android.com/tools">
+
+ <style name="Theme.Catalog" parent="@android:style/Theme.Material.Light.NoActionBar">
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowLightNavigationBar" tools:targetApi="o_mr1">true</item>
+ </style>
+
+</resources>
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
index 8b6a2df4..dbb3ff1 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/ScaffoldSamples.kt
@@ -24,8 +24,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
@@ -57,12 +57,9 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.roundToInt
@@ -260,16 +257,15 @@
}
@Sampled
-@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
@Composable
fun ScaffoldWithCoroutinesSnackbar() {
// decouple snackbar host state from scaffold state for demo purposes
// this state, channel and flow is for demo purposes to demonstrate business logic layer
val snackbarHostState = remember { SnackbarHostState() }
// we allow only one snackbar to be in the queue here, hence conflated
- val channel = remember { BroadcastChannel<Int>(Channel.Factory.CONFLATED) }
+ val channel = remember { Channel<Int>(Channel.Factory.CONFLATED) }
LaunchedEffect(channel) {
- channel.asFlow().collect { index ->
+ channel.receiveAsFlow().collect { index ->
val result = snackbarHostState.showSnackbar(
message = "Snackbar # $index",
actionLabel = "Action on $index"
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
index 51f0eb4..021cac7 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/TextFieldSamples.kt
@@ -197,7 +197,7 @@
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
- keyboardController?.hideSoftwareKeyboard()
+ keyboardController?.hide()
// do something here
}
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index 5d9c108..2dcf21b 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.material
import android.os.SystemClock.sleep
+import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -277,6 +278,97 @@
@Test
@LargeTest
+ fun modalDrawer_animateTo(): Unit = runBlocking(AutoTestFrameClock()) {
+ lateinit var drawerState: DrawerState
+ rule.setMaterialContent {
+ drawerState = rememberDrawerState(DrawerValue.Closed)
+ ModalDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag("drawer"))
+ },
+ content = {}
+ )
+ }
+
+ val width = rule.rootWidth()
+
+ // Drawer should start in closed state
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+
+ // When the drawer state is set to Opened
+ drawerState.animateTo(DrawerValue.Open, TweenSpec())
+ // Then the drawer should be opened
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
+
+ // When the drawer state is set to Closed
+ drawerState.animateTo(DrawerValue.Closed, TweenSpec())
+ // Then the drawer should be closed
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+ }
+
+ @Test
+ @LargeTest
+ fun modalDrawer_snapTo(): Unit = runBlocking(AutoTestFrameClock()) {
+ lateinit var drawerState: DrawerState
+ rule.setMaterialContent {
+ drawerState = rememberDrawerState(DrawerValue.Closed)
+ ModalDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag("drawer"))
+ },
+ content = {}
+ )
+ }
+
+ val width = rule.rootWidth()
+
+ // Drawer should start in closed state
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+
+ // When the drawer state is set to Opened
+ drawerState.snapTo(DrawerValue.Open)
+ // Then the drawer should be opened
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
+
+ // When the drawer state is set to Closed
+ drawerState.snapTo(DrawerValue.Closed)
+ // Then the drawer should be closed
+ rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
+ }
+
+ @Test
+ @LargeTest
+ fun modalDrawer_currentValue(): Unit = runBlocking(AutoTestFrameClock()) {
+ lateinit var drawerState: DrawerState
+ rule.setMaterialContent {
+ drawerState = rememberDrawerState(DrawerValue.Closed)
+ ModalDrawer(
+ drawerState = drawerState,
+ drawerContent = {
+ Box(Modifier.fillMaxSize().testTag("drawer"))
+ },
+ content = {}
+ )
+ }
+
+ // Drawer should start in closed state
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+
+ // When the drawer state is set to Opened
+ drawerState.snapTo(DrawerValue.Open)
+ // Then the drawer should be opened
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+
+ // When the drawer state is set to Closed
+ drawerState.snapTo(DrawerValue.Closed)
+ // Then the drawer should be closed
+ assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+ }
+
+ @Test
+ @LargeTest
fun modalDrawer_bodyContent_clickable(): Unit = runBlocking(AutoTestFrameClock()) {
var drawerClicks = 0
var bodyClicks = 0
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
index 7160529..668bf10 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ProgressIndicatorTest.kt
@@ -15,8 +15,14 @@
*/
package androidx.compose.material
+import android.os.Build
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.assertPixelColor
+import androidx.compose.testutils.assertPixels
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toPixelMap
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.test.ExperimentalTestApi
@@ -25,11 +31,15 @@
import androidx.compose.ui.test.assertRangeInfoEquals
import androidx.compose.ui.test.assertValueEquals
import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -173,4 +183,63 @@
contentToTest
.assertIsSquareWithSize(40.dp)
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun determinateLinearProgressIndicator_sizeModifier() {
+ val expectedWidth = 100.dp
+ val expectedHeight = 10.dp
+ val expectedSize = with(rule.density) {
+ IntSize(expectedWidth.roundToPx(), expectedHeight.roundToPx())
+ }
+ val tag = "progress_indicator"
+ rule.setContent {
+ LinearProgressIndicator(
+ modifier = Modifier.testTag(tag).size(expectedWidth, expectedHeight),
+ progress = 1f,
+ color = Color.Blue
+ )
+ }
+
+ rule.onNodeWithTag(tag)
+ .captureToImage()
+ .assertPixels(expectedSize = expectedSize) {
+ Color.Blue
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ @Test
+ fun indeterminateLinearProgressIndicator_sizeModifier() {
+ val expectedWidth = 100.dp
+ val expectedHeight = 10.dp
+ val expectedSize = with(rule.density) {
+ IntSize(expectedWidth.roundToPx(), expectedHeight.roundToPx())
+ }
+ rule.mainClock.autoAdvance = false
+ val tag = "progress_indicator"
+ rule.setContent {
+
+ LinearProgressIndicator(
+ modifier = Modifier.testTag(tag).size(expectedWidth, expectedHeight),
+ color = Color.Blue
+ )
+ }
+
+ rule.mainClock.advanceTimeBy(100)
+
+ rule.onNodeWithTag(tag)
+ .captureToImage()
+ .toPixelMap()
+ .let {
+ assertEquals(expectedSize.width, it.width)
+ assertEquals(expectedSize.height, it.height)
+ // Assert on the first pixel column, to make sure that the progress indicator draws
+ // to the expect height.
+ // We can't assert width as the width dynamically changes during the animation
+ for (i in 0 until it.height) {
+ it.assertPixelColor(Color.Blue, 0, i)
+ }
+ }
+ }
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 969ccea..8e21331 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -324,7 +324,7 @@
rule.runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isTrue() }
// Hide keyboard.
- rule.runOnIdle { softwareKeyboardController?.hideSoftwareKeyboard() }
+ rule.runOnIdle { softwareKeyboardController?.hide() }
// Clicking on the text field shows the keyboard.
rule.onNodeWithTag(TextfieldTag).performClick()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
index b766467..abce8b2 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/AppBar.kt
@@ -22,8 +22,8 @@
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
@@ -168,6 +168,9 @@
* See [BottomAppBar anatomy](https://material.io/components/app-bars-bottom/#anatomy) for the
* recommended content depending on the [FloatingActionButton] position.
*
+ * Note that when you pass a non-null [cutoutShape] this makes the AppBar shape concave. The shadows
+ * for such shapes will not be drawn on Android versions less than 10.
+ *
* @sample androidx.compose.material.samples.SimpleBottomAppBar
*
* @param modifier The [Modifier] to be applied to this BottomAppBar
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index e623c4d..7009d90 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -16,6 +16,7 @@
package androidx.compose.material
+import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -33,6 +34,7 @@
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
@@ -78,6 +80,7 @@
/**
* Possible values of [BottomDrawerState].
*/
+@ExperimentalMaterialApi
enum class BottomDrawerValue {
/**
* The state of the bottom drawer when it is closed.
@@ -101,17 +104,20 @@
* @param initialValue The initial value of the state.
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*/
-@Suppress("NotCloseable", "HiddenSuperclass")
+@Suppress("NotCloseable")
@OptIn(ExperimentalMaterialApi::class)
@Stable
class DrawerState(
initialValue: DrawerValue,
confirmStateChange: (DrawerValue) -> Boolean = { true }
-) : SwipeableState<DrawerValue>(
- initialValue = initialValue,
- animationSpec = AnimationSpec,
- confirmStateChange = confirmStateChange
) {
+
+ internal val swipeableState = SwipeableState(
+ initialValue = initialValue,
+ animationSpec = AnimationSpec,
+ confirmStateChange = confirmStateChange
+ )
+
/**
* Whether the drawer is open.
*/
@@ -125,13 +131,33 @@
get() = currentValue == DrawerValue.Closed
/**
+ * The current value of the state.
+ *
+ * If no swipe or animation is in progress, this corresponds to the start the drawer
+ * currently in. If a swipe or an animation is in progress, this corresponds the state drawer
+ * was in before the swipe or animation started.
+ */
+ val currentValue: DrawerValue
+ get() {
+ return swipeableState.currentValue
+ }
+
+ /**
+ * Whether the state is currently animating.
+ */
+ val isAnimationRunning: Boolean
+ get() {
+ return swipeableState.isAnimationRunning
+ }
+
+ /**
* Open the drawer with animation and suspend until it if fully opened or animation has been
* cancelled. This method will throw [CancellationException] if the animation is
* interrupted
*
* @return the reason the open animation ended
*/
- suspend fun open() = animateTo(DrawerValue.Open)
+ suspend fun open() = animateTo(DrawerValue.Open, AnimationSpec)
/**
* Close the drawer with animation and suspend until it if fully closed or animation has been
@@ -140,7 +166,48 @@
*
* @return the reason the close animation ended
*/
- suspend fun close() = animateTo(DrawerValue.Closed)
+ suspend fun close() = animateTo(DrawerValue.Closed, AnimationSpec)
+
+ /**
+ * Set the state of the drawer with specific animation
+ *
+ * @param targetValue The new value to animate to.
+ * @param anim The animation that will be used to animate to the new value.
+ */
+ @ExperimentalMaterialApi
+ suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
+ swipeableState.animateTo(targetValue, anim)
+ }
+
+ /**
+ * Set the state without any animation and suspend until it's set
+ *
+ * @param targetValue The new target value
+ */
+ @ExperimentalMaterialApi
+ suspend fun snapTo(targetValue: DrawerValue) {
+ swipeableState.snapTo(targetValue)
+ }
+
+ /**
+ * The target value of the drawer state.
+ *
+ * If a swipe is in progress, this is the value that the Drawer would animate to if the
+ * swipe finishes. If an animation is running, this is the target value of that animation.
+ * Finally, if no swipe or animation is in progress, this is the same as the [currentValue].
+ */
+ @ExperimentalMaterialApi
+ @get:ExperimentalMaterialApi
+ val targetValue: DrawerValue
+ get() = swipeableState.targetValue
+
+ /**
+ * The current position (in pixels) of the drawer sheet.
+ */
+ @ExperimentalMaterialApi
+ @get:ExperimentalMaterialApi
+ val offset: State<Float>
+ get() = swipeableState.offset
companion object {
/**
@@ -160,8 +227,8 @@
* @param initialValue The initial value of the state.
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*/
-@Suppress("NotCloseable", "HiddenSuperclass")
-@OptIn(ExperimentalMaterialApi::class)
+@Suppress("NotCloseable")
+@ExperimentalMaterialApi
class BottomDrawerState(
initialValue: BottomDrawerValue,
confirmStateChange: (BottomDrawerValue) -> Boolean = { true }
@@ -260,6 +327,7 @@
* @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
*/
@Composable
+@ExperimentalMaterialApi
fun rememberBottomDrawerState(
initialValue: BottomDrawerValue,
confirmStateChange: (BottomDrawerValue) -> Boolean = { true }
@@ -330,7 +398,7 @@
}
Box(
Modifier.swipeable(
- state = drawerState,
+ state = drawerState.swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.5f) },
orientation = Orientation.Horizontal,
@@ -346,7 +414,9 @@
Scrim(
open = drawerState.isOpen,
onClose = { scope.launch { drawerState.close() } },
- fraction = { calculateFraction(minValue, maxValue, drawerState.offset.value) },
+ fraction = {
+ calculateFraction(minValue, maxValue, drawerState.offset.value)
+ },
color = scrimColor
)
Surface(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
index 58fe636..954b607 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
@@ -78,7 +78,7 @@
.size(LinearIndicatorWidth, LinearIndicatorHeight)
.focusable()
) {
- val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
+ val strokeWidth = size.height
drawLinearIndicatorBackground(backgroundColor, strokeWidth)
drawLinearIndicator(0f, progress, color, strokeWidth)
}
@@ -152,7 +152,7 @@
.size(LinearIndicatorWidth, LinearIndicatorHeight)
.focusable()
) {
- val strokeWidth = ProgressIndicatorDefaults.StrokeWidth.toPx()
+ val strokeWidth = size.height
drawLinearIndicatorBackground(backgroundColor, strokeWidth)
if (firstLineHead - firstLineTail > 0) {
drawLinearIndicator(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
index 2819fda..b1d5f24 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Scaffold.kt
@@ -19,8 +19,8 @@
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
@@ -240,16 +240,16 @@
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
- val fabPlaceables = subcompose(ScaffoldLayoutContent.Fab, fab).fastMap {
- it.measure(looseConstraints)
- }
+ val fabPlaceables =
+ subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable ->
+ measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 }
+ }
- val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
- val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
-
- // FAB distance from the left of the layout, taking into account LTR / RTL
- val fabLeftOffset = if (fabWidth != 0 && fabHeight != 0) {
- if (fabPosition == FabPosition.End) {
+ val fabPlacement = if (fabPlaceables.isNotEmpty()) {
+ val fabWidth = fabPlaceables.fastMaxBy { it.width }!!.width
+ val fabHeight = fabPlaceables.fastMaxBy { it.height }!!.height
+ // FAB distance from the left of the layout, taking into account LTR / RTL
+ val fabLeftOffset = if (fabPosition == FabPosition.End) {
if (layoutDirection == LayoutDirection.Ltr) {
layoutWidth - FabSpacing.roundToPx() - fabWidth
} else {
@@ -258,11 +258,7 @@
} else {
(layoutWidth - fabWidth) / 2
}
- } else {
- 0
- }
- val fabPlacement = if (fabWidth != 0 && fabHeight != 0) {
FabPlacement(
isDocked = isFabDocked,
left = fabLeftOffset,
@@ -281,30 +277,23 @@
}.fastMap { it.measure(looseConstraints) }
val bottomBarHeight = bottomBarPlaceables.fastMaxBy { it.height }?.height ?: 0
-
- val fabOffsetFromBottom = if (fabWidth != 0 && fabHeight != 0) {
+ val fabOffsetFromBottom = fabPlacement?.let {
if (bottomBarHeight == 0) {
- fabHeight + FabSpacing.roundToPx()
+ it.height + FabSpacing.roundToPx()
} else {
if (isFabDocked) {
// Total height is the bottom bar height + half the FAB height
- bottomBarHeight + (fabHeight / 2)
+ bottomBarHeight + (it.height / 2)
} else {
// Total height is the bottom bar height + the FAB height + the padding
// between the FAB and bottom bar
- bottomBarHeight + fabHeight + FabSpacing.roundToPx()
+ bottomBarHeight + it.height + FabSpacing.roundToPx()
}
}
- } else {
- 0
}
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
- snackbarHeight + if (fabOffsetFromBottom != 0) {
- fabOffsetFromBottom
- } else {
- bottomBarHeight
- }
+ snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight)
} else {
0
}
@@ -332,8 +321,10 @@
it.place(0, layoutHeight - bottomBarHeight)
}
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
- fabPlaceables.fastForEach {
- it.place(fabLeftOffset, layoutHeight - fabOffsetFromBottom)
+ fabPlacement?.let { placement ->
+ fabPlaceables.fastForEach {
+ it.place(placement.left, layoutHeight - fabOffsetFromBottom!!)
+ }
}
}
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
index 46eb88b..cd23a8e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Surface.kt
@@ -43,7 +43,8 @@
* 1) Clipping: Surface clips its children to the shape specified by [shape]
*
* 2) Elevation: Surface draws a shadow to represent depth, where [elevation] represents the
- * depth of this surface.
+ * depth of this surface. If the passed [shape] is concave the shadow will not be drawn on Android
+ * versions less than 10.
*
* 3) Borders: If [shape] has a border, then it will also be drawn.
*
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index 42acda2..9bee9d4 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -53,7 +53,6 @@
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
@@ -544,7 +543,6 @@
* in order to animate to the next state, even if the positional [thresholds] have not been reached.
*/
@ExperimentalMaterialApi
-@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Modifier.swipeable(
state: SwipeableState<T>,
anchors: Map<Float, T>,
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
index 74d6f2c..ef2fca4 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/CompositionLocalNamingDetector.kt
@@ -19,6 +19,7 @@
package androidx.compose.runtime.lint
import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
import com.android.tools.lint.client.api.UElementHandler
import com.android.tools.lint.detector.api.Category
import com.android.tools.lint.detector.api.Detector
@@ -28,7 +29,6 @@
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.util.InheritanceUtil
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.uast.UElement
@@ -54,7 +54,7 @@
if ((node.sourcePsi as? KtProperty)?.isLocal == true) return
val type = node.type
- if (!InheritanceUtil.isInheritor(type, Names.Runtime.CompositionLocal.javaFqn)) return
+ if (!type.inheritsFrom(Names.Runtime.CompositionLocal)) return
val name = node.name
if (name!!.startsWith("Local", ignoreCase = true)) return
diff --git a/compose/runtime/runtime-saveable-lint/build.gradle b/compose/runtime/runtime-saveable-lint/build.gradle
new file mode 100644
index 0000000..d2cc46e
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * 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.BundleInsideHelper
+import androidx.build.LibraryGroups
+import androidx.build.LibraryType
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+BundleInsideHelper.forInsideLintJar(project)
+
+dependencies {
+ // compileOnly because we use lintChecks and it doesn't allow other types of deps
+ // this ugly hack exists because of b/63873667
+ if (rootProject.hasProperty("android.injected.invoked.from.ide")) {
+ compileOnly(LINT_API_LATEST)
+ } else {
+ compileOnly(LINT_API_MIN)
+ }
+ compileOnly(KOTLIN_STDLIB)
+ bundleInside(project(":compose:lint:common"))
+
+ testImplementation(KOTLIN_STDLIB)
+ testImplementation(LINT_CORE)
+ testImplementation(LINT_TESTS)
+}
+
+androidx {
+ name = "Compose Runtime Saveable Lint Checks"
+ type = LibraryType.LINT
+ mavenGroup = LibraryGroups.Compose.RUNTIME
+ inceptionYear = "2021"
+ description = "Compose Runtime Saveable Lint Checks"
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt
new file mode 100644
index 0000000..146c33a
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetector.kt
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import androidx.compose.lint.Name
+import androidx.compose.lint.Names
+import androidx.compose.lint.Package
+import androidx.compose.lint.inheritsFrom
+import androidx.compose.lint.isInPackageName
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LintFix
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import java.util.EnumSet
+
+/**
+ * [Detector] that checks `rememberSaveable` calls to make sure that a `Saver` is not passed to
+ * the vararg argument.
+ */
+class RememberSaveableDetector : Detector(), SourceCodeScanner {
+ override fun getApplicableMethodNames(): List<String> = listOf(RememberSaveable.shortName)
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ if (!method.isInPackageName(RuntimeSaveablePackageName)) return
+
+ val argumentMapping = context.evaluator.computeArgumentMapping(node, method)
+ // Filter the arguments provided to those that correspond to the varargs parameter.
+ val varargArguments = argumentMapping.toList().filter { (_, parameter) ->
+ // TODO: https://youtrack.jetbrains.com/issue/KT-45700 parameter.isVarArgs
+ // returns false because only varargs parameters at the end of a function are
+ // considered varargs in PSI, since Java callers can not call them as a vararg
+ // parameter otherwise. This is true for both KtLightParameterImpl, and
+ // ClsParameterImpl, so if we wanted to actually find if the parameter was
+ // vararg for Kotlin callers we would instead need to look through the metadata on
+ // the class file, or the source KtParameter.
+ // Instead since they are just treated as an array type in PSI, just find the
+ // corresponding array parameter.
+ parameter.type is PsiArrayType
+ }.map {
+ // We don't need the parameter anymore, so just return the argument
+ it.first
+ }
+
+ // Ignore if there are no vararg arguments provided, and ignore if there are multiple (we
+ // assume if that multiple are provided then the developer knows what they are doing)
+ if (varargArguments.size != 1) return
+
+ val argument = varargArguments.first()
+ val argumentType = argument.getExpressionType()
+
+ // Ignore if the expression isn't a `Saver`
+ if (argumentType?.inheritsFrom(Saver) != true) return
+
+ // If the type is a MutableState, there is a second overload with a differently
+ // named parameter we should use instead
+ val isMutableStateSaver = node.getExpressionType()
+ ?.inheritsFrom(Names.Runtime.MutableState) == true
+
+ // TODO: might be safer to try and find the other overload through PSI, and get
+ // the parameter name directly.
+ val parameterName = if (isMutableStateSaver) {
+ "stateSaver"
+ } else {
+ "saver"
+ }
+
+ val argumentText = argument.sourcePsi?.text
+
+ context.report(
+ RememberSaveableSaverParameter,
+ node,
+ context.getLocation(argument),
+ "Passing `Saver` instance to vararg `inputs`",
+ argumentText?.let {
+ val replacement = "$parameterName = $argumentText"
+ LintFix.create()
+ .replace()
+ .name("Change to `$replacement`")
+ .text(argumentText)
+ .with(replacement)
+ .autoFix()
+ .build()
+ }
+ )
+ }
+
+ companion object {
+ val RememberSaveableSaverParameter = Issue.create(
+ "RememberSaveableSaverParameter",
+ "`Saver` objects should be passed to the saver parameter, not the vararg " +
+ "`inputs` parameter",
+ "The first parameter to `rememberSaveable` is a vararg parameter for inputs that when" +
+ " changed will cause the state to reset. Passing a `Saver` object to this " +
+ "parameter is an error, as the intention is to pass the `Saver` object to the " +
+ "saver parameter. Since the saver parameter is not the first parameter, it must " +
+ "be explicitly named.",
+ Category.CORRECTNESS, 3, Severity.ERROR,
+ Implementation(
+ RememberSaveableDetector::class.java,
+ EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+ )
+ )
+ }
+}
+
+private val RuntimeSaveablePackageName = Package("androidx.compose.runtime.saveable")
+private val RememberSaveable = Name(RuntimeSaveablePackageName, "rememberSaveable")
+private val Saver = Name(RuntimeSaveablePackageName, "Saver")
diff --git a/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.kt b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.kt
new file mode 100644
index 0000000..89dda1a
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/java/androidx/compose/runtime/saveable/lint/RuntimeSaveableIssueRegistry.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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import com.android.tools.lint.client.api.IssueRegistry
+import com.android.tools.lint.detector.api.CURRENT_API
+
+/**
+ * [IssueRegistry] containing runtime-saveable specific lint issues.
+ */
+class RuntimeSaveableIssueRegistry : IssueRegistry() {
+ // Tests are run with this version. We ensure that with ApiLintVersionsTest
+ override val api = 8
+ override val minApi = CURRENT_API
+ override val issues get() = listOf(
+ RememberSaveableDetector.RememberSaveableSaverParameter
+ )
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
new file mode 100644
index 0000000..ff96940
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
@@ -0,0 +1 @@
+androidx.compose.runtime.saveable.lint.RuntimeSaveableIssueRegistry
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt
new file mode 100644
index 0000000..9bc4684
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/ApiLintVersionsTest.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import com.android.tools.lint.client.api.LintClient
+import com.android.tools.lint.detector.api.CURRENT_API
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class ApiLintVersionsTest {
+
+ @Test
+ fun versionsCheck() {
+ LintClient.clientName = LintClient.CLIENT_UNIT_TESTS
+
+ val registry = RuntimeSaveableIssueRegistry()
+ // we hardcode version registry.api to the version that is used to run tests
+ assertThat(registry.api).isEqualTo(CURRENT_API)
+ // Intentionally fails in IDE, because we use different API version in
+ // studio and command line
+ assertThat(registry.minApi).isEqualTo(3)
+ }
+}
diff --git a/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
new file mode 100644
index 0000000..2e36deb
--- /dev/null
+++ b/compose/runtime/runtime-saveable-lint/src/test/java/androidx/compose/runtime/saveable/lint/RememberSaveableDetectorTest.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.saveable.lint
+
+import androidx.compose.lint.Stubs
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+// TODO: add tests for methods defined in class files when we update Lint to support bytecode()
+// test files
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+/**
+ * Test for [RememberSaveableDetector].
+ */
+class RememberSaveableDetectorTest : LintDetectorTest() {
+ override fun getDetector(): Detector = RememberSaveableDetector()
+
+ override fun getIssues(): MutableList<Issue> =
+ mutableListOf(RememberSaveableDetector.RememberSaveableSaverParameter)
+
+ private val rememberSaveableStub: TestFile = kotlin(
+ """
+ package androidx.compose.runtime.saveable
+
+ import androidx.compose.runtime.*
+
+ @Composable
+ fun <T : Any> rememberSaveable(
+ vararg inputs: Any?,
+ saver: Saver<T, out Any> = autoSaver(),
+ key: String? = null,
+ init: () -> T
+ ): T = init()
+
+ @Composable
+ fun <T> rememberSaveable(
+ vararg inputs: Any?,
+ stateSaver: Saver<T, out Any>,
+ key: String? = null,
+ init: () -> MutableState<T>
+ ): MutableState<T> = rememberSaveable(
+ *inputs,
+ saver = mutableStateSaver(stateSaver),
+ key = key,
+ init = init
+ )
+
+ interface Saver<Original, Saveable : Any>
+
+ @Suppress("UNCHECKED_CAST")
+ private fun <T> autoSaver(): Saver<T, Any> =
+ (Any() as Saver<T, Any>)
+
+ @Suppress("UNCHECKED_CAST")
+ private fun <T> mutableStateSaver(inner: Saver<T, out Any>) =
+ Any() as Saver<MutableState<T>, MutableState<Any?>>
+ """
+ )
+
+ @Test
+ fun saverPassedToVarargs() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+ import androidx.compose.runtime.saveable.*
+
+ class Foo
+ object FooSaver : Saver<Any, Any>
+ class FooSaver2 : Saver<Any, Any>
+ val fooSaver3 = object : Saver<Any, Any> {}
+ val fooSaver4 = FooSaver2()
+
+ @Composable
+ fun Test() {
+ val foo = rememberSaveable(FooSaver) { Foo() }
+ val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
+ val foo2 = rememberSaveable(FooSaver2()) { Foo() }
+ val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
+ val foo3 = rememberSaveable(fooSaver3) { Foo() }
+ val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
+ val foo4 = rememberSaveable(fooSaver4) { Foo() }
+ val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
+ }
+ """
+ ),
+ rememberSaveableStub,
+ kotlin(Stubs.Composable),
+ kotlin(Stubs.MutableState)
+ )
+ .run()
+ .expect(
+ """
+src/test/Foo.kt:15: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val foo = rememberSaveable(FooSaver) { Foo() }
+ ~~~~~~~~
+src/test/Foo.kt:16: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
+ ~~~~~~~~
+src/test/Foo.kt:17: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val foo2 = rememberSaveable(FooSaver2()) { Foo() }
+ ~~~~~~~~~~~
+src/test/Foo.kt:18: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
+ ~~~~~~~~~~~
+src/test/Foo.kt:19: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val foo3 = rememberSaveable(fooSaver3) { Foo() }
+ ~~~~~~~~~
+src/test/Foo.kt:20: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
+ ~~~~~~~~~
+src/test/Foo.kt:21: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val foo4 = rememberSaveable(fooSaver4) { Foo() }
+ ~~~~~~~~~
+src/test/Foo.kt:22: Error: Passing Saver instance to vararg inputs [RememberSaveableSaverParameter]
+ val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
+ ~~~~~~~~~
+8 errors, 0 warnings
+ """
+ )
+ .expectFixDiffs(
+ """
+Fix for src/test/Foo.kt line 15: Change to `saver = FooSaver`:
+@@ -15 +15
+- val foo = rememberSaveable(FooSaver) { Foo() }
++ val foo = rememberSaveable(saver = FooSaver) { Foo() }
+Fix for src/test/Foo.kt line 16: Change to `stateSaver = FooSaver`:
+@@ -16 +16
+- val mutableStateFoo = rememberSaveable(FooSaver) { mutableStateOf(Foo()) }
++ val mutableStateFoo = rememberSaveable(stateSaver = FooSaver) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 17: Change to `saver = FooSaver2()`:
+@@ -17 +17
+- val foo2 = rememberSaveable(FooSaver2()) { Foo() }
++ val foo2 = rememberSaveable(saver = FooSaver2()) { Foo() }
+Fix for src/test/Foo.kt line 18: Change to `stateSaver = FooSaver2()`:
+@@ -18 +18
+- val mutableStateFoo2 = rememberSaveable(FooSaver2()) { mutableStateOf(Foo()) }
++ val mutableStateFoo2 = rememberSaveable(stateSaver = FooSaver2()) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 19: Change to `saver = fooSaver3`:
+@@ -19 +19
+- val foo3 = rememberSaveable(fooSaver3) { Foo() }
++ val foo3 = rememberSaveable(saver = fooSaver3) { Foo() }
+Fix for src/test/Foo.kt line 20: Change to `stateSaver = fooSaver3`:
+@@ -20 +20
+- val mutableStateFoo3 = rememberSaveable(fooSaver3) { mutableStateOf(Foo()) }
++ val mutableStateFoo3 = rememberSaveable(stateSaver = fooSaver3) { mutableStateOf(Foo()) }
+Fix for src/test/Foo.kt line 21: Change to `saver = fooSaver4`:
+@@ -21 +21
+- val foo4 = rememberSaveable(fooSaver4) { Foo() }
++ val foo4 = rememberSaveable(saver = fooSaver4) { Foo() }
+Fix for src/test/Foo.kt line 22: Change to `stateSaver = fooSaver4`:
+@@ -22 +22
+- val mutableStateFoo4 = rememberSaveable(fooSaver4) { mutableStateOf(Foo()) }
++ val mutableStateFoo4 = rememberSaveable(stateSaver = fooSaver4) { mutableStateOf(Foo()) }
+ """
+ )
+ }
+
+ @Test
+ fun noErrors() {
+ lint().files(
+ kotlin(
+ """
+ package test
+
+ import androidx.compose.runtime.*
+ import androidx.compose.runtime.saveable.*
+
+ class Foo
+ object FooSaver : Saver<Any, Any>
+ class FooSaver2 : Saver<Any, Any>
+ val fooSaver3 = object : Saver<Any, Any> {}
+ val fooSaver4 = FooSaver2()
+
+ @Composable
+ fun Test() {
+ val foo = rememberSaveable(saver = FooSaver) { Foo() }
+ val mutableStateFoo = rememberSaveable(stateSaver = FooSaver) {
+ mutableStateOf(Foo())
+ }
+ val foo2 = rememberSaveable(saver = FooSaver2()) { Foo() }
+ val mutableStateFoo2 = rememberSaveable(stateSaver = FooSaver2()) {
+ mutableStateOf(Foo())
+ }
+ val foo3 = rememberSaveable(saver = fooSaver3) { Foo() }
+ val mutableStateFoo3 = rememberSaveable(stateSaver = fooSaver3) {
+ mutableStateOf(Foo())
+ }
+ val foo4 = rememberSaveable(saver = fooSaver4) { Foo() }
+ val mutableStateFoo4 = rememberSaveable(stateSaver = fooSaver4) {
+ mutableStateOf(Foo())
+ }
+
+ val fooVarargs = rememberSaveable(Any(), FooSaver, Any()) { Foo() }
+ val mutableStateFooVarargs = rememberSaveable(Any(), FooSaver, Any()) {
+ mutableStateOf(Foo())
+ }
+ }
+ """
+ ),
+ rememberSaveableStub,
+ kotlin(Stubs.Composable),
+ kotlin(Stubs.MutableState)
+ )
+ .run()
+ .expectClean()
+ }
+}
+/* ktlint-enable max-line-length */
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index d062d34..2f53274 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -60,6 +60,8 @@
// DexMaker has it"s own MockMaker
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy)
// DexMaker has it"s own MockMaker
+
+ lintPublish(project(":compose:runtime:runtime-saveable-lint"))
}
}
diff --git a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
index 2e204db..033b693 100644
--- a/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
+++ b/compose/runtime/runtime-saveable/src/androidAndroidTest/kotlin/androidx/compose/runtime/saveable/RememberSaveableTest.kt
@@ -382,6 +382,34 @@
assertThat(actualKey).isNotEmpty()
}
+
+ @Test
+ fun restoreCorrectValueAfterInputChanges() {
+ var counter = 0
+ var composedValue: Int? = null
+ var input by mutableStateOf(0)
+ restorationTester.setContent {
+ composedValue = rememberSaveable(input) {
+ counter++
+ }
+ }
+
+ rule.runOnIdle {
+ assertThat(composedValue).isEqualTo(0)
+ input = 1 // this will reset the state
+ }
+
+ rule.runOnIdle {
+ assertThat(composedValue).isEqualTo(1)
+ composedValue = null // clear to make sure the restoration worked
+ }
+
+ restorationTester.emulateSavedInstanceStateRestore()
+
+ rule.runOnIdle {
+ assertThat(composedValue).isEqualTo(1)
+ }
+ }
}
@Composable
diff --git a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
index e1cb14e..e1e5146 100644
--- a/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
+++ b/compose/runtime/runtime-saveable/src/commonMain/kotlin/androidx/compose/runtime/saveable/RememberSaveable.kt
@@ -95,7 +95,7 @@
// re-register if the registry or key has been changed
if (registry != null) {
- DisposableEffect(registry, finalKey) {
+ DisposableEffect(registry, finalKey, value) {
val valueProvider = {
with(saverHolder.value) { SaverScope { registry.canBeSaved(it) }.save(value) }
}
diff --git a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
index 579ca44..61ff963 100644
--- a/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
+++ b/compose/runtime/runtime/integration-tests/src/androidAndroidTest/kotlin/androidx/compose/runtime/FlowAdapterTest.kt
@@ -16,16 +16,13 @@
package androidx.compose.runtime
+import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import androidx.compose.ui.test.junit4.createComposeRule
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.BroadcastChannel
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asFlow
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -34,29 +31,22 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
-@ExperimentalCoroutinesApi
-@FlowPreview
class FlowAdapterTest {
- private class FlowChannel<T> {
- val channel = BroadcastChannel<T>(1)
- val flow = channel.asFlow()
- }
-
@get:Rule
val rule = createComposeRule()
@Test
fun weReceiveSubmittedValue() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(initial = null).value
+ realValue = stream.collectAsState(initial = null).value
}
rule.runOnIdle {
- stream.channel.offer("value")
+ stream.tryEmit("value")
}
rule.runOnIdle {
@@ -66,20 +56,20 @@
@Test
fun weReceiveSecondValue() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(initial = null).value
+ realValue = stream.collectAsState(initial = null).value
}
rule.runOnIdle {
- stream.channel.offer("value")
+ stream.tryEmit("value")
}
rule.runOnIdle {
assertThat(realValue).isEqualTo("value")
- stream.channel.offer("value2")
+ stream.tryEmit("value2")
}
rule.runOnIdle {
@@ -89,19 +79,19 @@
@Test
fun noUpdatesAfterDispose() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var emit by mutableStateOf(true)
var realValue: String? = "to-be-updated"
rule.setContent {
if (emit) {
- realValue = stream.flow.collectAsState(initial = null).value
+ realValue = stream.collectAsState(initial = null).value
}
}
rule.runOnIdle { emit = false }
rule.runOnIdle {
- stream.channel.offer("value")
+ stream.tryEmit("value")
}
rule.runOnIdle {
@@ -111,10 +101,10 @@
@Test
fun testCollectionWithInitialValue() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var realValue = "to-be-updated"
rule.setContent {
- realValue = stream.flow.collectAsState("value").value
+ realValue = stream.collectAsState("value").value
}
assertThat(realValue).isEqualTo("value")
@@ -122,14 +112,14 @@
@Test
fun testOverridingInitialValue() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var realValue = "to-be-updated"
rule.setContent {
- realValue = stream.flow.collectAsState("value").value
+ realValue = stream.collectAsState("value").value
}
rule.runOnIdle {
- stream.channel.offer("value2")
+ stream.tryEmit("value2")
}
rule.runOnIdle {
@@ -139,12 +129,12 @@
@Test
fun theCurrentValueIsNotLostWhenWeUpdatedInitial() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var initial by mutableStateOf("initial1")
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(initial).value
+ realValue = stream.collectAsState(initial).value
}
rule.runOnIdle {
@@ -158,13 +148,13 @@
@Test
fun replacingStreams() {
- val stream1 = FlowChannel<String>()
- val stream2 = FlowChannel<String>()
+ val stream1 = MutableSharedFlow<String>(extraBufferCapacity = 1)
+ val stream2 = MutableSharedFlow<String>(extraBufferCapacity = 1)
var stream by mutableStateOf(stream1)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(initial = null).value
+ realValue = stream.collectAsState(initial = null).value
}
rule.runOnIdle {
@@ -172,11 +162,11 @@
}
rule.runOnIdle {
- stream2.channel.offer("stream2")
+ stream2.tryEmit("stream2")
}
rule.runOnIdle {
- stream1.channel.offer("stream1")
+ stream1.tryEmit("stream1")
}
rule.runOnIdle {
@@ -186,17 +176,17 @@
@Test
fun theCurrentValueIsNotLostWhenWeReplacedStreams() {
- val stream1 = FlowChannel<String>()
- val stream2 = FlowChannel<String>()
+ val stream1 = MutableSharedFlow<String>(extraBufferCapacity = 1)
+ val stream2 = MutableSharedFlow<String>(extraBufferCapacity = 1)
var stream by mutableStateOf(stream1)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(initial = null).value
+ realValue = stream.collectAsState(initial = null).value
}
rule.runOnIdle {
- stream1.channel.offer("value")
+ stream1.tryEmit("value")
}
rule.runOnIdle {
@@ -211,15 +201,15 @@
@Ignore("b/177256608")
@Test
fun observingOnCustomContext() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(null, Dispatchers.Default).value
+ realValue = stream.collectAsState(null, Dispatchers.Default).value
}
rule.runOnUiThread {
- stream.channel.offer("value")
+ stream.tryEmit("value")
}
rule.waitUntil { realValue != null }
@@ -228,16 +218,16 @@
@Test
fun theCurrentValueIsNotLostWhenWeReplacedContext() {
- val stream = FlowChannel<String>()
+ val stream = MutableSharedFlow<String>(extraBufferCapacity = 1)
var context by mutableStateOf<CoroutineContext>(Dispatchers.Main)
var realValue: String? = null
rule.setContent {
- realValue = stream.flow.collectAsState(null, context).value
+ realValue = stream.collectAsState(null, context).value
}
rule.runOnIdle {
- stream.channel.offer("value")
+ stream.tryEmit("value")
}
rule.runOnIdle {
diff --git a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
index 8a20b7d..733a59a 100644
--- a/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
+++ b/compose/runtime/runtime/samples/src/main/java/androidx/compose/runtime/samples/FlowAdapterSamples.kt
@@ -21,7 +21,6 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -34,7 +33,6 @@
@Sampled
@Composable
-@ExperimentalCoroutinesApi
fun StateFlowSample(stateFlow: StateFlow<String>) {
val value: String by stateFlow.collectAsState()
Text("Value is $value")
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
index 024c2b5..6739924 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BitwiseOperators.kt
@@ -17,8 +17,12 @@
@file:Suppress("NOTHING_TO_INLINE")
package androidx.compose.runtime
+// NOTE: rotateRight, marked @ExperimentalStdlibApi is also marked inline-only,
+// which makes this usage stable.
@OptIn(ExperimentalStdlibApi::class)
internal inline infix fun Int.ror(other: Int) = this.rotateRight(other)
+// NOTE: rotateLeft, marked @ExperimentalStdlibApi is also marked inline-only,
+// which makes this usage stable.
@OptIn(ExperimentalStdlibApi::class)
internal inline infix fun Int.rol(other: Int) = this.rotateLeft(other)
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index d970e3c..558a505 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -1331,6 +1331,11 @@
sideEffects += effect
}
+ val hasEffects: Boolean
+ get() = sideEffects.isNotEmpty() ||
+ forgetting.isNotEmpty() ||
+ remembering.isNotEmpty()
+
fun dispatchRememberObservers() {
// Send forgets
if (forgetting.isNotEmpty()) {
@@ -1361,11 +1366,13 @@
fun dispatchAbandons() {
if (abandoning.isNotEmpty()) {
- val iterator = abandoning.iterator()
- while (iterator.hasNext()) {
- val instance = iterator.next()
- iterator.remove()
- instance.onAbandoned()
+ trace("Compose:dispatchAbandons") {
+ val iterator = abandoning.iterator()
+ while (iterator.hasNext()) {
+ val instance = iterator.next()
+ iterator.remove()
+ instance.onAbandoned()
+ }
}
}
}
@@ -1410,8 +1417,12 @@
// Side effects run after lifecycle observers so that any remembered objects
// that implement RememberObserver receive onRemembered before a side effect
// that captured it and operates on it can run.
- manager.dispatchRememberObservers()
- manager.dispatchSideEffects()
+ if (manager.hasEffects) {
+ trace("Compose:dispatchEffects") {
+ manager.dispatchRememberObservers()
+ manager.dispatchSideEffects()
+ }
+ }
if (pendingInvalidScopes) {
pendingInvalidScopes = false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 3da5ece..14629c8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -413,10 +413,10 @@
// each time, but because we've installed the broadcastFrameClock as the scope
// clock above for user code to locate.
parentFrameClock.withFrameNanos { frameTime ->
- trace("recomposeFrame") {
- // Dispatch MonotonicFrameClock frames first; this may produce new
- // composer invalidations that we must handle during the same frame.
- if (broadcastFrameClock.hasAwaiters) {
+ // Dispatch MonotonicFrameClock frames first; this may produce new
+ // composer invalidations that we must handle during the same frame.
+ if (broadcastFrameClock.hasAwaiters) {
+ trace("Recomposer:animation") {
// Propagate the frame time to anyone who is awaiting from the
// recomposer clock.
broadcastFrameClock.sendFrame(frameTime)
@@ -424,7 +424,9 @@
// Ensure any global changes are observed
Snapshot.sendApplyNotifications()
}
+ }
+ trace("Recomposer:recompose") {
// Drain any composer invalidations from snapshot changes and record
// composers to work on
synchronized(stateLock) {
@@ -531,10 +533,10 @@
// each time, but because we've installed the broadcastFrameClock as the scope
// clock above for user code to locate.
parentFrameClock.withFrameNanos { frameTime ->
- trace("recomposeFrame") {
- // Dispatch MonotonicFrameClock frames first; this may produce new
- // composer invalidations that we must handle during the same frame.
- if (broadcastFrameClock.hasAwaiters) {
+ // Dispatch MonotonicFrameClock frames first; this may produce new
+ // composer invalidations that we must handle during the same frame.
+ if (broadcastFrameClock.hasAwaiters) {
+ trace("Recomposer:animation") {
// Propagate the frame time to anyone who is awaiting from the
// recomposer clock.
broadcastFrameClock.sendFrame(frameTime)
@@ -542,7 +544,9 @@
// Ensure any global changes are observed
Snapshot.sendApplyNotifications()
}
+ }
+ trace("Recomposer:recompose") {
// Drain any composer invalidations from snapshot changes and record
// composers to work on.
// We'll do these synchronously to make the current frame.
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 4307a69..3c49b27 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-@file:Suppress("UNCHECKED_CAST")
@file:OptIn(InternalComposeApi::class)
package androidx.compose.runtime.internal
@@ -47,173 +46,42 @@
@Stable
@OptIn(ComposeCompilerApi::class)
/* ktlint-disable parameter-list-wrapping */ // TODO(https://github.com/pinterest/ktlint/issues/921): reenable
-internal class ComposableLambdaImpl(
- val key: Int,
- private val tracked: Boolean,
- private val sourceInformation: String?
+internal expect class ComposableLambdaImpl(
+ key: Int,
+ tracked: Boolean,
+ sourceInformation: String?
) : ComposableLambda {
- private var _block: Any? = null
- private var scope: RecomposeScope? = null
- private var scopes: MutableList<RecomposeScope>? = null
+ fun update(block: Any)
+}
- private fun trackWrite() {
- if (tracked) {
- val scope = this.scope
- if (scope != null) {
- scope.invalidate()
- this.scope = null
- }
- val scopes = this.scopes
- if (scopes != null) {
- for (index in 0 until scopes.size) {
- val item = scopes[index]
- item.invalidate()
- }
- scopes.clear()
- }
- }
- }
-
- private fun trackRead(composer: Composer) {
- if (tracked) {
- val scope = composer.recomposeScope
- if (scope != null) {
- // Find the first invalid scope and replace it or record it if no scopes are invalid
- composer.recordUsed(scope)
- val lastScope = this.scope
- if (lastScope.replacableWith(scope)) {
- this.scope = scope
- } else {
- val lastScopes = scopes
- if (lastScopes == null) {
- val newScopes = mutableListOf<RecomposeScope>()
- scopes = newScopes
- newScopes.add(scope)
- } else {
- for (index in 0 until lastScopes.size) {
- val scopeAtIndex = lastScopes[index]
- if (scopeAtIndex.replacableWith(scope)) {
- lastScopes[index] = scope
- return
- }
- }
- lastScopes.add(scope)
- }
- }
- }
- }
- }
-
- fun update(block: Any) {
- if (_block != block) {
- val oldBlockNull = _block == null
- _block = block
- if (!oldBlockNull) {
- trackWrite()
- }
- }
- }
-
- override operator fun invoke(c: Composer, changed: Int): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(0) else sameBits(0)
- val result = (_block as (c: Composer, changed: Int) -> Any?)(c, dirty)
- c.endRestartGroup()?.updateScope(this as (Composer, Int) -> Unit)
- return result
- }
-
- override operator fun invoke(p1: Any?, c: Composer, changed: Int): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(1) else sameBits(1)
- val result = (
- _block as (
- p1: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- c,
- dirty
+internal fun RecomposeScope?.replacableWith(other: RecomposeScope) =
+ this == null || (
+ this is RecomposeScopeImpl && other is RecomposeScopeImpl && (
+ !this.valid || this == other || this.anchor == other.anchor
+ )
)
- c.endRestartGroup()?.updateScope { nc, _ -> this(p1, nc, changed or 0b1) }
- return result
- }
- override operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(2) else sameBits(2)
- val result = (_block as (p1: Any?, p2: Any?, c: Composer, changed: Int) -> Any?)(
- p1,
- p2,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, nc, changed or 0b1) }
- return result
- }
+@ComposeCompilerApi
+@Stable
+expect interface ComposableLambda {
+ operator fun invoke(c: Composer, changed: Int): Any?
- override operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(3) else sameBits(3)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, p3, nc, changed or 0b1) }
- return result
- }
+ operator fun invoke(p1: Any?, c: Composer, changed: Int): Any?
- override operator fun invoke(
+ operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any?
+
+ operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any?
+
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
p4: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(4) else sameBits(4)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -221,36 +89,9 @@
p5: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(5) else sameBits(5)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -259,38 +100,9 @@
p6: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(6) else sameBits(6)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -300,40 +112,9 @@
p7: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(7) else sameBits(7)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -344,42 +125,9 @@
p8: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(8) else sameBits(8)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, p8, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -391,44 +139,9 @@
p9: Any?,
c: Composer,
changed: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed or if (c.changed(this)) differentBits(9) else sameBits(9)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- c: Composer,
- changed: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- c,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, p8, p9, nc, changed or 0b1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -442,48 +155,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(10) else sameBits(10)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, nc, changed or 0b1, changed)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -498,50 +172,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(11) else sameBits(11)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, nc, changed or 0b1, changed1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -557,52 +190,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(12) else sameBits(12)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, nc, changed or 0b1, changed1)
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -619,71 +209,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(13) else sameBits(13)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -701,74 +229,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(14) else sameBits(14)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- p14: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -787,77 +250,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(15) else sameBits(15)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- p14: Any?,
- p15: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -877,80 +272,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(16) else sameBits(16)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- p14: Any?,
- p15: Any?,
- p16: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -971,83 +295,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(17) else sameBits(17)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- p14: Any?,
- p15: Any?,
- p16: Any?,
- p17: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- p17,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- p17,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
- override operator fun invoke(
+ operator fun invoke(
p1: Any?,
p2: Any?,
p3: Any?,
@@ -1069,126 +319,9 @@
c: Composer,
changed: Int,
changed1: Int
- ): Any? {
- val c = c.startRestartGroup(key, sourceInformation)
- trackRead(c)
- val dirty = changed1 or if (c.changed(this)) differentBits(18) else sameBits(18)
- val result = (
- _block as (
- p1: Any?,
- p2: Any?,
- p3: Any?,
- p4: Any?,
- p5: Any?,
- p6: Any?,
- p7: Any?,
- p8: Any?,
- p9: Any?,
- p10: Any?,
- p11: Any?,
- p12: Any?,
- p13: Any?,
- p14: Any?,
- p15: Any?,
- p16: Any?,
- p17: Any?,
- p18: Any?,
- c: Composer,
- changed: Int,
- changed1: Int
- ) -> Any?
- )(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- p17,
- p18,
- c,
- changed,
- dirty
- )
- c.endRestartGroup()?.updateScope { nc, _ ->
- this(
- p1,
- p2,
- p3,
- p4,
- p5,
- p6,
- p7,
- p8,
- p9,
- p10,
- p11,
- p12,
- p13,
- p14,
- p15,
- p16,
- p17,
- p18,
- nc,
- changed or 0b1,
- changed1
- )
- }
- return result
- }
+ ): Any?
}
-private fun RecomposeScope?.replacableWith(other: RecomposeScope) =
- this == null || (
- this is RecomposeScopeImpl && other is RecomposeScopeImpl && (
- !this.valid || this == other || this.anchor == other.anchor
- )
- )
-
-@ComposeCompilerApi
-@Stable
-interface ComposableLambda :
- Function2<Composer, Int, Any?>,
- Function3<Any?, Composer, Int, Any?>,
- Function4<Any?, Any?, Composer, Int, Any?>,
- Function5<Any?, Any?, Any?, Composer, Int, Any?>,
- Function6<Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function7<Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function8<Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function9<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function10<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function11<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
- Function13<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
- Any?>,
- Function14<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
- Any?>,
- Function15<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer,
- Int, Int, Any?>,
- Function16<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Composer, Int, Int, Any?>,
- Function17<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Composer, Int,
- Int, Any?>,
- Function18<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Any?, Composer, Int, Int, Any?>,
- Function19<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Any?, Any?, Composer, Int, Int, Any?>,
- Function20<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Any?, Any?, Any?, Composer, Int, Int, Any?>,
- Function21<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
- Any?, Any?, Any?, Any?, Composer, Int, Int, Any?>
-
@Suppress("unused")
@ComposeCompilerApi
fun composableLambda(
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt
new file mode 100644
index 0000000..8fe6f28
--- /dev/null
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.jvm.kt
@@ -0,0 +1,1171 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("UNCHECKED_CAST")
+@file:OptIn(InternalComposeApi::class)
+package androidx.compose.runtime.internal
+
+import androidx.compose.runtime.ComposeCompilerApi
+import androidx.compose.runtime.Composer
+import androidx.compose.runtime.InternalComposeApi
+import androidx.compose.runtime.RecomposeScope
+import androidx.compose.runtime.Stable
+
+/**
+ * A Restart is created to hold composable lambdas to track when they are invoked allowing
+ * the invocations to be invalidated when a new composable lambda is created during composition.
+ *
+ * This allows much of the call-graph to be skipped when a composable function is passed through
+ * multiple levels of composable functions.
+ */
+@Suppress("NAME_SHADOWING")
+@Stable
+@OptIn(ComposeCompilerApi::class)
+/* ktlint-disable parameter-list-wrapping */ // TODO(https://github.com/pinterest/ktlint/issues/921): reenable
+internal actual class ComposableLambdaImpl actual constructor(
+ val key: Int,
+ private val tracked: Boolean,
+ private val sourceInformation: String?
+) : ComposableLambda {
+ private var _block: Any? = null
+ private var scope: RecomposeScope? = null
+ private var scopes: MutableList<RecomposeScope>? = null
+
+ private fun trackWrite() {
+ if (tracked) {
+ val scope = this.scope
+ if (scope != null) {
+ scope.invalidate()
+ this.scope = null
+ }
+ val scopes = this.scopes
+ if (scopes != null) {
+ for (index in 0 until scopes.size) {
+ val item = scopes[index]
+ item.invalidate()
+ }
+ scopes.clear()
+ }
+ }
+ }
+
+ private fun trackRead(composer: Composer) {
+ if (tracked) {
+ val scope = composer.recomposeScope
+ if (scope != null) {
+ // Find the first invalid scope and replace it or record it if no scopes are invalid
+ composer.recordUsed(scope)
+ val lastScope = this.scope
+ if (lastScope.replacableWith(scope)) {
+ this.scope = scope
+ } else {
+ val lastScopes = scopes
+ if (lastScopes == null) {
+ val newScopes = mutableListOf<RecomposeScope>()
+ scopes = newScopes
+ newScopes.add(scope)
+ } else {
+ for (index in 0 until lastScopes.size) {
+ val scopeAtIndex = lastScopes[index]
+ if (scopeAtIndex.replacableWith(scope)) {
+ lastScopes[index] = scope
+ return
+ }
+ }
+ lastScopes.add(scope)
+ }
+ }
+ }
+ }
+ }
+
+ actual fun update(block: Any) {
+ if (_block != block) {
+ val oldBlockNull = _block == null
+ _block = block
+ if (!oldBlockNull) {
+ trackWrite()
+ }
+ }
+ }
+
+ override operator fun invoke(c: Composer, changed: Int): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(0) else sameBits(0)
+ val result = (_block as (c: Composer, changed: Int) -> Any?)(c, dirty)
+ c.endRestartGroup()?.updateScope(this as (Composer, Int) -> Unit)
+ return result
+ }
+
+ override operator fun invoke(p1: Any?, c: Composer, changed: Int): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(1) else sameBits(1)
+ val result = (
+ _block as (
+ p1: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ -> this(p1, nc, changed or 0b1) }
+ return result
+ }
+
+ override operator fun invoke(p1: Any?, p2: Any?, c: Composer, changed: Int): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(2) else sameBits(2)
+ val result = (_block as (p1: Any?, p2: Any?, c: Composer, changed: Int) -> Any?)(
+ p1,
+ p2,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, nc, changed or 0b1) }
+ return result
+ }
+
+ override operator fun invoke(p1: Any?, p2: Any?, p3: Any?, c: Composer, changed: Int): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(3) else sameBits(3)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ -> this(p1, p2, p3, nc, changed or 0b1) }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(4) else sameBits(4)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(5) else sameBits(5)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(6) else sameBits(6)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(7) else sameBits(7)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(8) else sameBits(8)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, p8, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ c: Composer,
+ changed: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed or if (c.changed(this)) differentBits(9) else sameBits(9)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ c: Composer,
+ changed: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ c,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, p8, p9, nc, changed or 0b1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(10) else sameBits(10)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, nc, changed or 0b1, changed)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(11) else sameBits(11)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, nc, changed or 0b1, changed1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(12) else sameBits(12)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, nc, changed or 0b1, changed1)
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(13) else sameBits(13)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(14) else sameBits(14)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(15) else sameBits(15)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(16) else sameBits(16)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ p17: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(17) else sameBits(17)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ p17: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ p17,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ p17,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+
+ override operator fun invoke(
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ p17: Any?,
+ p18: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ): Any? {
+ val c = c.startRestartGroup(key, sourceInformation)
+ trackRead(c)
+ val dirty = changed1 or if (c.changed(this)) differentBits(18) else sameBits(18)
+ val result = (
+ _block as (
+ p1: Any?,
+ p2: Any?,
+ p3: Any?,
+ p4: Any?,
+ p5: Any?,
+ p6: Any?,
+ p7: Any?,
+ p8: Any?,
+ p9: Any?,
+ p10: Any?,
+ p11: Any?,
+ p12: Any?,
+ p13: Any?,
+ p14: Any?,
+ p15: Any?,
+ p16: Any?,
+ p17: Any?,
+ p18: Any?,
+ c: Composer,
+ changed: Int,
+ changed1: Int
+ ) -> Any?
+ )(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ p17,
+ p18,
+ c,
+ changed,
+ dirty
+ )
+ c.endRestartGroup()?.updateScope { nc, _ ->
+ this(
+ p1,
+ p2,
+ p3,
+ p4,
+ p5,
+ p6,
+ p7,
+ p8,
+ p9,
+ p10,
+ p11,
+ p12,
+ p13,
+ p14,
+ p15,
+ p16,
+ p17,
+ p18,
+ nc,
+ changed or 0b1,
+ changed1
+ )
+ }
+ return result
+ }
+}
+
+@ComposeCompilerApi
+@Stable
+actual interface ComposableLambda :
+ Function2<Composer, Int, Any?>,
+ Function3<Any?, Composer, Int, Any?>,
+ Function4<Any?, Any?, Composer, Int, Any?>,
+ Function5<Any?, Any?, Any?, Composer, Int, Any?>,
+ Function6<Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function7<Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function8<Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function9<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function10<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function11<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Any?>,
+ Function13<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
+ Any?>,
+ Function14<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer, Int, Int,
+ Any?>,
+ Function15<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Composer,
+ Int, Int, Any?>,
+ Function16<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Composer, Int, Int, Any?>,
+ Function17<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Composer, Int,
+ Int, Any?>,
+ Function18<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Any?, Composer, Int, Int, Any?>,
+ Function19<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Any?, Any?, Composer, Int, Int, Any?>,
+ Function20<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Any?, Any?, Any?, Composer, Int, Int, Any?>,
+ Function21<Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?, Any?,
+ Any?, Any?, Any?, Any?, Composer, Int, Int, Any?>
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
index a5745b8..f2dc064 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionLocalTests.kt
@@ -450,36 +450,38 @@
validate()
}
+ @Composable
+ fun ReadSomeDataCompositionLocal(
+ compositionLocal: CompositionLocal<SomeData>,
+ composed: StableRef<Boolean>,
+ ) {
+ composed.value = true
+ Text(value = compositionLocal.current.value)
+ }
+
@Test
fun providingANewDataClassValueShouldNotRecompose() = compositionTest {
val invalidates = mutableListOf<RecomposeScope>()
fun doInvalidate() = invalidates.forEach { it.invalidate() }.also { invalidates.clear() }
val someDataCompositionLocal = compositionLocalOf(structuralEqualityPolicy()) { SomeData() }
- var composed = false
-
- @Composable
- fun ReadSomeDataCompositionLocal(
- compositionLocal: CompositionLocal<SomeData>
- ) {
- composed = true
- Text(value = compositionLocal.current.value)
- }
+ val composed = StableRef(false)
compose {
invalidates.add(currentRecomposeScope)
CompositionLocalProvider(
someDataCompositionLocal provides SomeData("provided")
) {
- ReadSomeDataCompositionLocal(someDataCompositionLocal)
+ ReadSomeDataCompositionLocal(someDataCompositionLocal, composed)
}
}
- assertTrue(composed)
- composed = false
+ assertTrue(composed.value)
+ composed.value = false
doInvalidate()
expectNoChanges()
- assertFalse(composed)
+ assertFalse(composed.value)
}
}
-private data class SomeData(val value: String = "default")
\ No newline at end of file
+data class SomeData(val value: String = "default")
+@Stable class StableRef<T>(var value: T)
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
index 807feef..7e8c149 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/LatchTest.kt
@@ -17,15 +17,12 @@
package androidx.compose.runtime
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.test.Test
import kotlin.test.assertTrue
-// OptIn for CoroutineStart.UNDISPATCHED
-@OptIn(ExperimentalCoroutinesApi::class)
class LatchTest {
@Test
fun openDoesntSuspend() = runBlocking {
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
index d69da23..012f6ee 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/NewCodeGenTests.kt
@@ -117,51 +117,50 @@
validate()
}
+ @Composable
+ fun PhoneView(phone: Phone, phoneCalled: StableCounter) {
+ phoneCalled.count++
+ Text(
+ "${if (phone.area.isBlank()) "" else "(${phone.area}) "}${
+ phone.prefix}-${phone.number}"
+ )
+ }
+
@Test
fun testComposableFunctionInvocationOneParameter() = compositionTest {
- data class Phone(val area: String, val prefix: String, val number: String)
-
var phone by mutableStateOf(Phone("123", "456", "7890"))
- var phoneCalled = 0
+ val phoneCalled = StableCounter()
var scope: RecomposeScope? = null
compose {
- @Composable
- fun PhoneView(phone: Phone) {
- phoneCalled++
- Text(
- "${if (phone.area.isBlank()) "" else "(${phone.area}) "}${
- phone.prefix}-${phone.number}"
- )
- }
scope = currentRecomposeScope
- PhoneView(phone)
+ PhoneView(phone, phoneCalled)
}
- assertEquals(1, phoneCalled)
+ assertEquals(1, phoneCalled.count)
scope?.invalidate()
advance()
- assertEquals(1, phoneCalled)
+ assertEquals(1, phoneCalled.count)
phone = Phone("124", "456", "7890")
advance()
- assertEquals(2, phoneCalled)
+ assertEquals(2, phoneCalled.count)
+ }
+
+ @Composable
+ fun AddView(left: Int, right: Int, addCalled: StableCounter) {
+ addCalled.count++
+ Text("$left + $right = ${left + right}")
}
@Test
fun testComposableFunctionInvocationTwoParameters() = compositionTest {
var left by mutableStateOf(0)
var right by mutableStateOf(1)
- var addCalled = 0
+ val addCalled = StableCounter()
var scope: RecomposeScope? = null
compose {
- @Composable
- fun AddView(left: Int, right: Int) {
- addCalled++
- Text("$left + $right = ${left + right}")
- }
-
scope = currentRecomposeScope
- AddView(left, right)
+ AddView(left, right, addCalled)
}
fun validate() {
@@ -170,27 +169,27 @@
}
}
validate()
- assertEquals(1, addCalled)
+ assertEquals(1, addCalled.count)
scope?.invalidate()
advance()
validate()
- assertEquals(1, addCalled)
+ assertEquals(1, addCalled.count)
left = 1
advance()
validate()
- assertEquals(2, addCalled)
+ assertEquals(2, addCalled.count)
scope?.invalidate()
advance()
validate()
- assertEquals(2, addCalled)
+ assertEquals(2, addCalled.count)
right = 41
advance()
validate()
- assertEquals(3, addCalled)
+ assertEquals(3, addCalled.count)
}
@Test
@@ -220,4 +219,9 @@
advance()
validate()
}
-}
\ No newline at end of file
+}
+
+@Stable
+class StableCounter(var count: Int = 0)
+
+data class Phone(val area: String, val prefix: String, val number: String)
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
index bc460846..40425a0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Outline.kt
@@ -97,7 +97,8 @@
/**
* An area defined as a path.
*
- * Note that only convex paths can be used for drawing the shadow. See [Path.isConvex].
+ * Note that if you use this path for drawing the shadow on Android versions less than 10 the
+ * shadow will not be drawn for the concave paths. See [Path.isConvex].
*/
class Generic(val path: Path) : Outline() {
override val bounds: Rect
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 6954ac7..01317e9 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
@@ -76,7 +76,6 @@
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -94,11 +93,6 @@
@RunWith(AndroidJUnit4::class)
class ParameterFactoryTest {
private val factory = ParameterFactory(InlineClassConverter())
- private val node = MutableInspectorNode().apply {
- width = 1000
- height = 500
- id = NODE_ID
- }.build()
@Before
fun before() {
@@ -258,7 +252,6 @@
)
}
- @Ignore
@Test
fun testCornerBasedShape() {
validate(create("corner", RoundedCornerShape(2.0.dp, 0.5.dp, 2.5.dp, 0.7.dp))) {
@@ -271,18 +264,18 @@
}
validate(create("corner", CutCornerShape(2))) {
parameter("corner", ParameterType.String, CutCornerShape::class.java.simpleName) {
- parameter("bottomEnd", ParameterType.DimensionDp, 5.0f)
- parameter("bottomStart", ParameterType.DimensionDp, 5.0f)
- parameter("topEnd", ParameterType.DimensionDp, 5.0f)
- parameter("topStart", ParameterType.DimensionDp, 5.0f)
+ parameter("bottomEnd", ParameterType.String, "2.0%")
+ parameter("bottomStart", ParameterType.String, "2.0%")
+ parameter("topEnd", ParameterType.String, "2.0%")
+ parameter("topStart", ParameterType.String, "2.0%")
}
}
validate(create("corner", RoundedCornerShape(1.0f, 10.0f, 2.0f, 3.5f))) {
parameter("corner", ParameterType.String, RoundedCornerShape::class.java.simpleName) {
- parameter("bottomEnd", ParameterType.DimensionDp, 1.0f)
- parameter("bottomStart", ParameterType.DimensionDp, 1.75f)
- parameter("topEnd", ParameterType.DimensionDp, 5.0f)
- parameter("topStart", ParameterType.DimensionDp, 0.5f)
+ parameter("bottomEnd", ParameterType.String, "2.0px")
+ parameter("bottomStart", ParameterType.String, "3.5px")
+ parameter("topEnd", ParameterType.String, "10.0px")
+ parameter("topStart", ParameterType.String, "1.0px")
}
}
}
@@ -431,6 +424,66 @@
}
@Test
+ fun testMap() {
+ val map = mapOf(1 to "one", 2 to "two")
+ validate(create("map", map)) {
+ parameter("map", ParameterType.Iterable, "Map[2]") {
+ parameter("[1]", ParameterType.String, "one") {
+ parameter("key", ParameterType.Int32, 1)
+ parameter("value", ParameterType.String, "one")
+ }
+ parameter("[2]", ParameterType.String, "two") {
+ parameter("key", ParameterType.Int32, 2)
+ parameter("value", ParameterType.String, "two")
+ }
+ }
+ }
+ }
+
+ @Test
+ fun testMapEntry() {
+ val entry = object : Map.Entry<String, String> {
+ override val key = "Hello"
+ override val value = "World"
+ }
+ validate(create("myEntry", entry)) {
+ parameter("myEntry", ParameterType.String, "World") {
+ parameter("key", ParameterType.String, "Hello")
+ parameter("value", ParameterType.String, "World")
+ }
+ }
+ }
+
+ @Test
+ fun testMapWithComplexTypes() {
+ val k1 = MyClass("k1")
+ val k2 = MyClass("k2")
+ val v1 = MyClass("v1")
+ val v2 = MyClass("v2")
+ val map = mapOf(k1 to v1, k2 to v2)
+ validate(create("map", map, maxRecursions = 3)) {
+ parameter("map", ParameterType.Iterable, "Map[2]") {
+ parameter("[MyClass]", ParameterType.String, "MyClass") {
+ parameter("key", ParameterType.String, "MyClass") {
+ parameter("name", ParameterType.String, "k1")
+ }
+ parameter("value", ParameterType.String, "MyClass") {
+ parameter("name", ParameterType.String, "v1")
+ }
+ }
+ parameter("[MyClass]", ParameterType.String, "MyClass") {
+ parameter("key", ParameterType.String, "MyClass") {
+ parameter("name", ParameterType.String, "k2")
+ }
+ parameter("value", ParameterType.String, "MyClass") {
+ parameter("name", ParameterType.String, "v2")
+ }
+ }
+ }
+ }
+ }
+
+ @Test
fun testShortIntArray() {
val value = intArrayOf(10, 11, 12)
val parameter = create("array", value)
@@ -836,7 +889,7 @@
): NodeParameter {
val parameter = factory.create(
ROOT_ID,
- node,
+ NODE_ID,
name,
value,
PARAM_INDEX,
@@ -869,7 +922,7 @@
): NodeParameter? =
factory.expand(
ROOT_ID,
- node,
+ NODE_ID,
name,
value,
reference,
@@ -980,7 +1033,7 @@
val msg = "$trace${parameter.name}"
assertWithMessage(msg).that(parameter.type).isEqualTo(type)
assertWithMessage(msg).that(parameter.index).isEqualTo(expectedIndex)
- assertWithMessage(msg).that(checkEquals(parameter.reference, ref)).isTrue()
+ assertWithMessage(msg).that(parameter.reference.toString()).isEqualTo(ref.toString())
if (type != ParameterType.Lambda || value != null) {
assertWithMessage(msg).that(parameter.value).isEqualTo(value)
}
@@ -1007,13 +1060,16 @@
var other: MyClass? = null
var self: MyClass? = null
var third: MyClass? = null
+
+ override fun hashCode(): Int = name.hashCode()
+ override fun equals(other: Any?): Boolean = name == (other as? MyClass)?.name
}
private fun NodeParameter.checkEquals(other: NodeParameter): Boolean {
assertThat(other.name).isEqualTo(name)
assertThat(other.type).isEqualTo(type)
assertThat(other.value).isEqualTo(value)
- assertThat(checkEquals(reference, other.reference)).isTrue()
+ assertThat(other.reference.toString()).isEqualTo(reference.toString())
assertThat(other.elements.size).isEqualTo(elements.size)
var hasReferences = reference != null
elements.forEachIndexed { i, element ->
@@ -1021,9 +1077,3 @@
}
return hasReferences
}
-
-private fun checkEquals(ref1: NodeParameterReference?, ref2: NodeParameterReference?): Boolean =
- ref1 === ref2 ||
- ref1?.nodeId == ref2?.nodeId &&
- ref1?.parameterIndex == ref2?.parameterIndex &&
- ref1?.indices.contentEquals(ref2?.indices)
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 305feee..36e1ba7 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -119,7 +119,7 @@
return node.parameters.mapIndexed { index, parameter ->
parameterFactory.create(
rootId,
- node,
+ node.id,
parameter.name,
parameter.value,
index,
@@ -149,7 +149,7 @@
val parameter = node.parameters[reference.parameterIndex]
return parameterFactory.expand(
rootId,
- node,
+ node.id,
parameter.name,
parameter.value,
reference,
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
index de5268d..23cfcc3 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/NodeParameterReference.kt
@@ -35,4 +35,10 @@
parameterIndex: Int,
indices: List<Int>
) : this(nodeId, parameterIndex, indices.asIntArray())
+
+ // For testing:
+ override fun toString(): String {
+ val suffix = if (indices.isNotEmpty()) ", ${indices.joinToString()}" else ""
+ return "[$nodeId, $parameterIndex$suffix]"
+ }
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
index 483feab..d1673b4 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/ParameterFactory.kt
@@ -121,7 +121,7 @@
*/
fun create(
rootId: Long,
- node: InspectorNode,
+ nodeId: Long,
name: String,
value: Any?,
parameterIndex: Int,
@@ -133,7 +133,7 @@
return reflectionScope.withReflectiveAccess {
creator.create(
rootId,
- node,
+ nodeId,
name,
value,
parameterIndex,
@@ -149,9 +149,10 @@
/**
* Create/expand the [NodeParameter] specified by [reference].
*
- * @param node is the [InspectorNode] with the id of [reference].nodeId.
- * @param name is the name of the [reference].parameterIndex'th parameter of [node].
- * @param value is the value of the [reference].parameterIndex'th parameter of [node].
+ * @param rootId is the root id of the specified [nodeId].
+ * @param nodeId is the [InspectorNode.id] of the node the parameter belongs to.
+ * @param name is the name of the [reference].parameterIndex'th parameter of the node.
+ * @param value is the value of the [reference].parameterIndex'th parameter of the node.
* @param startIndex is the index of the 1st wanted element of a List/Array.
* @param maxElements is the max number of elements wanted from a List/Array.
* @param maxRecursions is the max recursion into composite types starting from reference.
@@ -159,7 +160,7 @@
*/
fun expand(
rootId: Long,
- node: InspectorNode,
+ nodeId: Long,
name: String,
value: Any?,
reference: NodeParameterReference,
@@ -173,7 +174,7 @@
return reflectionScope.withReflectiveAccess {
creator.expand(
rootId,
- node,
+ nodeId,
name,
value,
reference,
@@ -315,7 +316,7 @@
*/
private inner class ParameterCreator {
private var rootId = 0L
- private var node: InspectorNode? = null
+ private var nodeId = 0L
private var parameterIndex = 0
private var maxRecursions = 0
private var maxInitialIterableSize = 0
@@ -328,7 +329,7 @@
fun create(
rootId: Long,
- node: InspectorNode,
+ nodeId: Long,
name: String,
value: Any?,
parameterIndex: Int,
@@ -336,15 +337,15 @@
maxInitialIterableSize: Int
): NodeParameter =
try {
- setup(rootId, node, parameterIndex, maxRecursions, maxInitialIterableSize)
- create(name, value) ?: createEmptyParameter(name)
+ setup(rootId, nodeId, parameterIndex, maxRecursions, maxInitialIterableSize)
+ create(name, value, null) ?: createEmptyParameter(name)
} finally {
setup()
}
fun expand(
rootId: Long,
- node: InspectorNode,
+ nodeId: Long,
name: String,
value: Any?,
reference: NodeParameterReference,
@@ -353,17 +354,21 @@
maxRecursions: Int,
maxInitialIterableSize: Int
): NodeParameter? {
- setup(rootId, node, reference.parameterIndex, maxRecursions, maxInitialIterableSize)
+ setup(rootId, nodeId, reference.parameterIndex, maxRecursions, maxInitialIterableSize)
+ var parent: Pair<String, Any?>? = null
var new = Pair(name, value)
for (i in reference.indices) {
+ parent = new
new = find(new.first, new.second, i) ?: return null
}
recursions = 0
valueIndex.addAll(reference.indices.asSequence())
val parameter = if (startIndex == 0) {
- create(new.first, new.second)
+ create(new.first, new.second, parent?.second)
} else {
- createFromCompositeValue(new.first, new.second, startIndex, maxElements)
+ createFromCompositeValue(
+ new.first, new.second, parent?.second, startIndex, maxElements
+ )
}
if (parameter == null && reference.indices.isEmpty()) {
return createEmptyParameter(name)
@@ -377,13 +382,13 @@
private fun setup(
newRootId: Long = 0,
- newNode: InspectorNode? = null,
+ newNodeId: Long = 0,
newParameterIndex: Int = 0,
maxRecursions: Int = 0,
maxInitialIterableSize: Int = 0
) {
rootId = newRootId
- node = newNode
+ nodeId = newNodeId
parameterIndex = newParameterIndex
this.maxRecursions = maxRecursions
this.maxInitialIterableSize = maxInitialIterableSize
@@ -395,17 +400,18 @@
}
}
- private fun create(name: String, value: Any?): NodeParameter? {
+ private fun create(name: String, value: Any?, parentValue: Any?): NodeParameter? {
if (value == null) {
return null
}
createFromSimpleValue(name, value)?.let { return it }
- val existing = valueIndexMap[value] ?: return createFromCompositeValue(name, value)
+ val existing =
+ valueIndexMap[value] ?: return createFromCompositeValue(name, value, parentValue)
// Do not decompose an instance we already decomposed.
// Instead reference the data that was already decomposed.
- return createReferenceToExistingValue(name, value, existing)
+ return createReferenceToExistingValue(name, value, parentValue, existing)
}
private fun createFromSimpleValue(name: String, value: Any?): NodeParameter? {
@@ -420,7 +426,6 @@
is Boolean -> NodeParameter(name, ParameterType.Boolean, value)
is ComposableLambda -> createFromCLambda(name, value)
is Color -> NodeParameter(name, ParameterType.Color, value.toArgb())
-// is CornerSize -> createFromCornerSize(name, value)
is Double -> NodeParameter(name, ParameterType.Double, value)
is Dp -> NodeParameter(name, DimensionDp, value.value)
is Enum<*> -> NodeParameter(name, ParameterType.String, value.toString())
@@ -444,6 +449,7 @@
private fun createFromCompositeValue(
name: String,
value: Any?,
+ parentValue: Any?,
startIndex: Int = 0,
maxElements: Int = maxInitialIterableSize
): NodeParameter? = when {
@@ -451,6 +457,10 @@
value is Modifier -> createFromModifier(name, value)
value is InspectableValue -> createFromInspectableValue(name, value)
value is Sequence<*> -> createFromSequence(name, value, value, startIndex, maxElements)
+ value is Map<*, *> ->
+ createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
+ value is Map.Entry<*, *> ->
+ createFromMapEntry(name, value, parentValue)
value is Iterable<*> ->
createFromSequence(name, value, value.asSequence(), startIndex, maxElements)
value.javaClass.isArray -> createFromArray(name, value, startIndex, maxElements)
@@ -464,6 +474,8 @@
value is Modifier -> findFromModifier(name, value, index)
value is InspectableValue -> findFromInspectableValue(value, index)
value is Sequence<*> -> findFromSequence(value, index)
+ value is Map<*, *> -> findFromSequence(value.asSequence(), index)
+ value is Map.Entry<*, *> -> findFromMapEntry(value, index)
value is Iterable<*> -> findFromSequence(value.asSequence(), index)
value.javaClass.isArray -> findFromArray(value, index)
value is Offset -> findFromOffset(value, index)
@@ -474,11 +486,12 @@
private fun createRecursively(
name: String,
value: Any?,
+ parentValue: Any?,
index: Int
): NodeParameter? {
valueIndex.add(index)
recursions++
- val parameter = create(name, value)?.apply {
+ val parameter = create(name, value, parentValue)?.apply {
this.index = index
}
recursions--
@@ -499,11 +512,14 @@
private fun createReferenceToExistingValue(
name: String,
value: Any?,
+ parentValue: Any?,
ref: NodeParameterReference
): NodeParameter? {
val remember = recursions
recursions = maxRecursions
- val parameter = createFromCompositeValue(name, value)?.apply { reference = ref }
+ val parameter = createFromCompositeValue(name, value, parentValue)?.apply {
+ reference = ref
+ }
recursions = remember
return parameter
}
@@ -523,7 +539,7 @@
}
val remember = recursions
recursions = maxRecursions
- val parameter = create("p", value)
+ val parameter = create("p", value, null)
recursions = remember
valueIndexMap.remove(value)
return parameter != null
@@ -539,7 +555,20 @@
if (value != null) {
val index = valueIndexToReference()
valueIndexMap[value] = index
- valueLazyReferenceMap.remove(value)?.forEach { it.reference = index }
+ }
+ return this
+ }
+
+ /**
+ * Remove the [value] of this [NodeParameter] if there are no child elements.
+ */
+ private fun NodeParameter.removeIfEmpty(value: Any?): NodeParameter {
+ if (value != null) {
+ if (elements.isEmpty()) {
+ valueIndexMap.remove(value)
+ }
+ val reference = valueIndexMap[value]
+ valueLazyReferenceMap.remove(value)?.forEach { it.reference = reference }
}
return this
}
@@ -558,7 +587,7 @@
}
private fun valueIndexToReference(): NodeParameterReference =
- NodeParameterReference(node!!.id, parameterIndex, valueIndex)
+ NodeParameterReference(nodeId, parameterIndex, valueIndex)
private fun createEmptyParameter(name: String): NodeParameter =
NodeParameter(name, ParameterType.String, "")
@@ -615,12 +644,6 @@
return valueLookup[value]?.let { NodeParameter(name, ParameterType.String, it) }
}
-// private fun createFromCornerSize(name: String, value: CornerSize): NodeParameter {
-// val size = Size(node!!.width.toFloat(), node!!.height.toFloat())
-// val pixels = value.toPx(size, density)
-// return NodeParameter(name, DimensionDp, with(density) { pixels.toDp().value })
-// }
-
// For now: select ResourceFontFont closest to W400 and Normal, and return the resId
private fun createFromFontListFamily(
name: String,
@@ -646,9 +669,9 @@
else -> {
val elements = parameter.store(value).elements
properties.values.mapIndexedNotNullTo(elements) { index, part ->
- createRecursively(part.name, valueOf(part, value), index)
+ createRecursively(part.name, valueOf(part, value), value, index)
}
- parameter
+ parameter.removeIfEmpty(value)
}
}
}
@@ -707,9 +730,9 @@
}
val elements = parameter.store(value).elements
value.inspectableElements.mapIndexedNotNullTo(elements) { index, element ->
- createRecursively(element.name, element.value, index)
+ createRecursively(element.name, element.value, value, index)
}
- return parameter
+ return parameter.removeIfEmpty(value)
}
private fun findFromInspectableValue(
@@ -724,6 +747,29 @@
return Pair(element.name, element.value)
}
+ private fun createFromMapEntry(
+ name: String,
+ entry: Map.Entry<*, *>,
+ parentValue: Any?
+ ): NodeParameter? {
+ val key = createRecursively("key", entry.key, entry, 0) ?: return null
+ val value = createRecursively("value", entry.value, entry, 1) ?: return null
+ val keyName = (key.value?.toString() ?: "").ifEmpty { "entry" }
+ val valueName = value.value?.toString()?.ifEmpty { null }
+ val nodeName = if (parentValue is Map<*, *>) "[$keyName]" else name
+ return NodeParameter(nodeName, ParameterType.String, valueName).apply {
+ elements.add(key)
+ elements.add(value)
+ }
+ }
+
+ private fun findFromMapEntry(entry: Map.Entry<*, *>, index: Int): Pair<String, Any?>? =
+ when (index) {
+ 0 -> Pair("key", entry.key)
+ 1 -> Pair("value", entry.value)
+ else -> null
+ }
+
private fun createFromSequence(
name: String,
value: Any,
@@ -740,7 +786,9 @@
val rest = sequence.drop(startIndex).iterator()
var index = startIndex
while (rest.hasNext() && elements.size < maxElements) {
- createRecursively("[$index]", rest.next(), index)?.let { elements.add(it) }
+ createRecursively("[$index]", rest.next(), value, index)?.let {
+ elements.add(it)
+ }
index++
}
while (rest.hasNext()) {
@@ -749,7 +797,7 @@
break
}
}
- parameter
+ parameter.removeIfEmpty(value)
}
}
}
@@ -770,6 +818,7 @@
is CharArray -> "CharArray[${value.size}]"
is List<*> -> "List[${value.size}]"
is Set<*> -> "Set[${value.size}]"
+ is Map<*, *> -> "Map[${value.size}]"
is Collection<*> -> "Collection[${value.size}]"
is Iterable<*> -> "Iterable"
else -> "Sequence"
@@ -789,9 +838,9 @@
else -> {
val elements = parameter.elements
modifiers.mapIndexedNotNullTo(elements) { index, element ->
- createRecursively("", element, index)
+ createRecursively("", element, value, index)
}
- parameter.store(value)
+ parameter.store(value).removeIfEmpty(value)
}
}
}
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
index 7a8eb63..1797d44 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierDeclarationDetector.kt
@@ -19,6 +19,7 @@
package androidx.compose.ui.lint
import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
import androidx.compose.lint.isComposable
import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ComposableModifierFactory
import androidx.compose.ui.lint.ModifierDeclarationDetector.Companion.ModifierFactoryReturnType
@@ -34,7 +35,6 @@
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiType
-import com.intellij.psi.util.InheritanceUtil
import org.jetbrains.kotlin.psi.KtCallableDeclaration
import org.jetbrains.kotlin.psi.KtDeclarationWithBody
import org.jetbrains.kotlin.psi.KtFunction
@@ -66,7 +66,7 @@
val returnType = node.returnType ?: return
// Ignore functions that do not return Modifier or something implementing Modifier
- if (!InheritanceUtil.isInheritor(returnType, Names.Ui.Modifier.javaFqn)) return
+ if (!returnType.inheritsFrom(Names.Ui.Modifier)) return
val source = node.sourcePsi
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
index a0f2fa1..89159ee 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
@@ -19,6 +19,7 @@
package androidx.compose.ui.lint
import androidx.compose.lint.Names
+import androidx.compose.lint.inheritsFrom
import androidx.compose.lint.isComposable
import androidx.compose.lint.returnsUnit
import com.android.tools.lint.client.api.UElementHandler
@@ -31,7 +32,6 @@
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
-import com.intellij.psi.util.InheritanceUtil
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtParameter
import org.jetbrains.uast.UElement
@@ -61,8 +61,7 @@
if (!node.returnsUnit) return
val modifierParameter = node.uastParameters.firstOrNull { parameter ->
- parameter.sourcePsi is KtParameter &&
- InheritanceUtil.isInheritor(parameter.type, Names.Ui.Modifier.javaFqn)
+ parameter.sourcePsi is KtParameter && parameter.type.inheritsFrom(Names.Ui.Modifier)
} ?: return
// Need to strongly type this or else Kotlinc cannot resolve overloads for
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 3287fd4..ca66316 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -44,7 +44,6 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.junit.Ignore
@@ -153,7 +152,6 @@
wasIdleAfterApplySnapshot = composeIdlingResource.isIdleNow
// Record idleness after the first recomposition
- @OptIn(ExperimentalCoroutinesApi::class)
scope.async(start = CoroutineStart.UNDISPATCHED) {
// Await a single recomposition
withFrameNanos {}
diff --git a/compose/ui/ui-text/api/1.0.0-beta04.txt b/compose/ui/ui-text/api/1.0.0-beta04.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/1.0.0-beta04.txt
@@ -482,6 +482,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -482,6 +482,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
index 4ac26f3..fc1a6db 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_1.0.0-beta04.txt
@@ -488,6 +488,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 4ac26f3..fc1a6db 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -488,6 +488,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt b/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui-text/api/restricted_1.0.0-beta04.txt
@@ -482,6 +482,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 6c4d62d..43d56d1 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -482,6 +482,9 @@
public final class LayoutIntrinsicsKt {
}
+ public final class SpannedExtensionsKt {
+ }
+
public final class TempListUtilsKt {
}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
index 3587a8a..530c75b 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationTest.kt
@@ -3834,6 +3834,34 @@
}
@Test
+ fun testGetWordBoundary_spaces() {
+ val text = "ab cd e"
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontFamily = fontFamilyMeasureFont,
+ fontSize = 20.sp
+ )
+ )
+
+ // end of word (length+1) will select word
+ val singleSpaceStartResult = paragraph.getWordBoundary(text.indexOf('b') + 1)
+ assertThat(singleSpaceStartResult.start).isEqualTo(text.indexOf('a'))
+ assertThat(singleSpaceStartResult.end).isEqualTo(text.indexOf('b') + 1)
+
+ // beginning of word will select word
+ val singleSpaceEndResult = paragraph.getWordBoundary(text.indexOf('c'))
+ assertThat(singleSpaceEndResult.start).isEqualTo(text.indexOf('c'))
+ assertThat(singleSpaceEndResult.end).isEqualTo(text.indexOf('d') + 1)
+
+ // between spaces ("_ | _") where | is the requested offset and _ is the space, will
+ // return the exact collapsed range at offset/offset
+ val doubleSpaceResult = paragraph.getWordBoundary(text.indexOf('d') + 2)
+ assertThat(doubleSpaceResult.start).isEqualTo(text.indexOf('d') + 2)
+ assertThat(doubleSpaceResult.end).isEqualTo(text.indexOf('d') + 2)
+ }
+
+ @Test
fun testGetWordBoundary_Bidi() {
val text = "abc \u05d0\u05d1\u05d2 def"
val paragraph = simpleParagraph(
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
index a3e3778..7b87d30 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidParagraph.android.kt
@@ -267,7 +267,7 @@
)
}
- private val wordBoundary: WordBoundary by lazy {
+ private val wordBoundary: WordBoundary by lazy(LazyThreadSafetyMode.NONE) {
WordBoundary(textLocale, layout.text)
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
index 2fd4c74..08eb74e 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
@@ -47,13 +47,13 @@
resourceLoader: Font.ResourceLoader
) : ParagraphIntrinsics {
- override val minIntrinsicWidth: Float by lazy {
+ override val minIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
infoList.fastMaxBy {
it.intrinsics.minIntrinsicWidth
}?.intrinsics?.minIntrinsicWidth ?: 0f
}
- override val maxIntrinsicWidth: Float by lazy {
+ override val maxIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
infoList.fastMaxBy {
it.intrinsics.maxIntrinsicWidth
}?.intrinsics?.maxIntrinsicWidth ?: 0f
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
index cbbdc9f..47e4b661 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Paragraph.kt
@@ -221,7 +221,7 @@
/**
* Returns the TextRange of the word at the given character offset. Characters not
* part of a word, such as spaces, symbols, and punctuation, have word breaks
- * on both sides. In such cases, this method will return TextRange(offset, offset+1).
+ * on both sides. In such cases, this method will return TextRange(offset, offset).
* Word boundaries are defined more precisely in Unicode Standard Annex #29
* http://www.unicode.org/reports/tr29/#Word_Boundaries
*/
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
index 80ae2fc..e11d9cb 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/font/FontFamily.kt
@@ -39,7 +39,7 @@
*
* @sample androidx.compose.ui.text.samples.FontFamilySansSerifSample
*
- * @see [CSS sans-serif](https://www.w3.org/TR/css-fonts-3/#sans-serif)
+ * See [CSS sans-serif](https://www.w3.org/TR/css-fonts-3/#sans-serif)
*/
val SansSerif = GenericFontFamily("sans-serif")
@@ -48,7 +48,7 @@
*
* @sample androidx.compose.ui.text.samples.FontFamilySerifSample
*
- * @see [CSS serif](https://www.w3.org/TR/css-fonts-3/#serif)
+ * See [CSS serif](https://www.w3.org/TR/css-fonts-3/#serif)
*/
val Serif = GenericFontFamily("serif")
@@ -57,7 +57,7 @@
*
* @sample androidx.compose.ui.text.samples.FontFamilyMonospaceSample
*
- * @see [CSS monospace](https://www.w3.org/TR/css-fonts-3/#monospace)
+ * See [CSS monospace](https://www.w3.org/TR/css-fonts-3/#monospace)
*/
val Monospace = GenericFontFamily("monospace")
@@ -69,7 +69,7 @@
*
* @sample androidx.compose.ui.text.samples.FontFamilyCursiveSample
*
- * @see [CSS cursive](https://www.w3.org/TR/css-fonts-3/#cursive)
+ * See [CSS cursive](https://www.w3.org/TR/css-fonts-3/#cursive)
*/
val Cursive = GenericFontFamily("cursive")
}
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
index 18b58e1..bf40c4f 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/EditCommand.kt
@@ -36,7 +36,7 @@
/**
* Commit final [text] to the text box and set the new cursor position.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#commitText(java.lang.CharSequence,%20int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#commitText(java.lang.CharSequence,%20int)>
*/
class CommitTextCommand(
/**
@@ -111,7 +111,7 @@
/**
* Mark a certain region of text as composing text.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingRegion(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingRegion(int,%2520int)>
*/
class SetComposingRegionCommand(
/**
@@ -169,7 +169,7 @@
* Replace the currently composing text with the given text, and set the new cursor position. Any
* composing text set previously will be removed automatically.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingText(java.lang.CharSequence,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setComposingText(java.lang.CharSequence,%2520int)>
*/
class SetComposingTextCommand(
/**
@@ -255,7 +255,7 @@
* Before and after refer to the order of the characters in the string, not to their visual
* representation.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingText(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingText(int,%2520int)>
*/
class DeleteSurroundingTextCommand(
/**
@@ -307,7 +307,7 @@
* * This command does nothing if there are one or more invalid surrogate pairs
* in the requested range.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingTextInCodePoints(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#deleteSurroundingTextInCodePoints(int,%2520int)>
*/
class DeleteSurroundingTextInCodePointsCommand(
/**
@@ -380,7 +380,7 @@
* Sets the selection on the text. When [start] and [end] have the same value, it sets the cursor
* position.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setSelection(int,%2520int)>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#setSelection(int,%2520int)>
*/
class SetSelectionCommand(
/**
@@ -429,7 +429,7 @@
* removing any special composing styling or other state that was around it. The cursor position
* remains unchanged.
*
- * @see <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#finishComposingText()>
+ * See <https://developer.android.com/reference/android/view/inputmethod/InputConnection.html#finishComposingText()>
*/
class FinishComposingTextCommand : EditCommand {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
index 6924d17..e145592 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt
@@ -87,6 +87,7 @@
* There is no guarantee that the keyboard will be shown. The software keyboard or
* system service may silently ignore this request.
*/
+ // TODO(b/183448615) @InternalTextApi
fun showSoftwareKeyboard() {
if (_currentInputSession.get() != null) {
platformTextInputService.showSoftwareKeyboard()
@@ -96,6 +97,7 @@
/**
* Hide onscreen keyboard.
*/
+ // TODO(b/183448615) @InternalTextApi
fun hideSoftwareKeyboard(): Unit = platformTextInputService.hideSoftwareKeyboard()
}
/**
@@ -194,7 +196,7 @@
* @return false if this session expired and no action was performed
*/
fun showSoftwareKeyboard(): Boolean = ensureOpenSession {
- textInputService.showSoftwareKeyboard()
+ platformTextInputService.showSoftwareKeyboard()
}
/**
@@ -209,7 +211,7 @@
* @return false if this session expired and no action was performed
*/
fun hideSoftwareKeyboard(): Boolean = ensureOpenSession {
- textInputService.hideSoftwareKeyboard()
+ platformTextInputService.hideSoftwareKeyboard()
}
}
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
index 6e2e895..0ddf205 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.desktop.kt
@@ -53,6 +53,7 @@
import org.jetbrains.skija.Typeface
import org.jetbrains.skija.paragraph.Alignment as SkAlignment
import org.jetbrains.skija.paragraph.BaselineMode
+import org.jetbrains.skija.paragraph.Direction as SkDirection
import org.jetbrains.skija.paragraph.LineMetrics
import org.jetbrains.skija.paragraph.ParagraphBuilder
import org.jetbrains.skija.paragraph.ParagraphStyle
@@ -184,13 +185,17 @@
return path
}
- private val cursorWidth = 2.0f
- override fun getCursorRect(offset: Int) =
- getBoxForwardByOffset(offset)?.let { box ->
- Rect(box.rect.left, box.rect.top, box.rect.left + cursorWidth, box.rect.bottom)
- } ?: getBoxBackwardByOffset(offset)?.let { box ->
- Rect(box.rect.right, box.rect.top, box.rect.right + cursorWidth, box.rect.bottom)
- } ?: Rect(0f, 0f, cursorWidth, paragraphIntrinsics.builder.defaultHeight)
+ override fun getCursorRect(offset: Int): Rect {
+ val horizontal = getHorizontalPosition(offset, true)
+ val line = lineMetricsForOffset(offset)!!
+
+ return Rect(
+ horizontal,
+ (line.baseline - line.ascent).toFloat(),
+ horizontal,
+ (line.baseline + line.descent).toFloat()
+ )
+ }
override fun getLineLeft(lineIndex: Int): Float =
lineMetrics.getOrNull(lineIndex)?.left?.toFloat() ?: 0f
@@ -258,10 +263,22 @@
}
override fun getHorizontalPosition(offset: Int, usePrimaryDirection: Boolean): Float {
- return if (usePrimaryDirection) {
- getHorizontalPositionForward(offset) ?: getHorizontalPositionBackward(offset) ?: 0f
- } else {
- getHorizontalPositionBackward(offset) ?: getHorizontalPositionForward(offset) ?: 0f
+ val prevBox = getBoxBackwardByOffset(offset)
+ val nextBox = getBoxForwardByOffset(offset)
+ return when {
+ prevBox == null -> {
+ val line = lineMetricsForOffset(offset)!!
+ return when (getParagraphDirection(offset)) {
+ ResolvedTextDirection.Ltr -> line.left.toFloat()
+ ResolvedTextDirection.Rtl -> line.right.toFloat()
+ }
+ }
+
+ nextBox == null || usePrimaryDirection || nextBox.direction == prevBox.direction ->
+ prevBox.cursorHorizontalPosition()
+
+ else ->
+ nextBox.cursorHorizontalPosition(true)
}
}
@@ -314,17 +331,15 @@
return null
}
- private fun getHorizontalPositionForward(from: Int) =
- getBoxForwardByOffset(from)?.rect?.left
-
- private fun getHorizontalPositionBackward(to: Int) =
- getBoxBackwardByOffset(to)?.rect?.right
-
override fun getParagraphDirection(offset: Int): ResolvedTextDirection =
- ResolvedTextDirection.Ltr
+ paragraphIntrinsics.textDirection
override fun getBidiRunDirection(offset: Int): ResolvedTextDirection =
- ResolvedTextDirection.Ltr
+ when (getBoxForwardByOffset(offset)?.direction) {
+ org.jetbrains.skija.paragraph.Direction.RTL -> ResolvedTextDirection.Rtl
+ org.jetbrains.skija.paragraph.Direction.LTR -> ResolvedTextDirection.Ltr
+ null -> ResolvedTextDirection.Ltr
+ }
override fun getOffsetForPosition(position: Offset): Int {
return para.getGlyphPositionAtCoordinate(position.x, position.y).position
@@ -336,11 +351,15 @@
}
override fun getWordBoundary(offset: Int): TextRange {
- if (text[offset].isWhitespace()) {
- return TextRange(offset, offset)
- }
- para.getWordBoundary(offset).let {
- return TextRange(it.start, it.end)
+ return when {
+ (text[offset].isLetterOrDigit()) -> para.getWordBoundary(offset).let {
+ TextRange(it.start, it.end)
+ }
+ (text.getOrNull(offset - 1)?.isLetterOrDigit() ?: false) ->
+ para.getWordBoundary(offset - 1).let {
+ TextRange(it.start, it.end)
+ }
+ else -> TextRange(offset, offset)
}
}
@@ -534,7 +553,8 @@
var maxLines: Int = Int.MAX_VALUE,
val spanStyles: List<Range<SpanStyle>>,
val placeholders: List<Range<Placeholder>>,
- val density: Density
+ val density: Density,
+ val textDirection: ResolvedTextDirection
) {
private lateinit var initialStyle: SpanStyle
private lateinit var defaultStyle: ComputedStyle
@@ -744,6 +764,7 @@
style.textAlign?.let {
pStyle.alignment = it.toSkAlignment()
}
+ pStyle.direction = textDirection.toSkDirection()
return pStyle
}
@@ -824,11 +845,11 @@
}
}
-fun Shadow.toSkShadow(): SkShadow {
+internal fun Shadow.toSkShadow(): SkShadow {
return SkShadow(color.toArgb(), offset.x, offset.y, blurRadius.toDouble())
}
-fun TextAlign.toSkAlignment(): SkAlignment {
+internal fun TextAlign.toSkAlignment(): SkAlignment {
return when (this) {
TextAlign.Left -> SkAlignment.LEFT
TextAlign.Right -> SkAlignment.RIGHT
@@ -838,3 +859,17 @@
TextAlign.End -> SkAlignment.END
}
}
+
+internal fun ResolvedTextDirection.toSkDirection(): SkDirection {
+ return when (this) {
+ ResolvedTextDirection.Ltr -> SkDirection.LTR
+ ResolvedTextDirection.Rtl -> SkDirection.RTL
+ }
+}
+
+internal fun TextBox.cursorHorizontalPosition(opposite: Boolean = false): Float {
+ return when (direction) {
+ SkDirection.LTR, null -> if (opposite) rect.left else rect.right
+ SkDirection.RTL -> if (opposite) rect.right else rect.left
+ }
+}
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
index 5aca88f..1860b3c 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraphIntrinsics.desktop.kt
@@ -21,6 +21,9 @@
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.resolveTextDirection
+import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Density
import org.jetbrains.skija.paragraph.Paragraph
import kotlin.math.ceil
@@ -52,6 +55,7 @@
) : ParagraphIntrinsics {
val fontLoader = resourceLoader as FontLoader
+ val textDirection = resolveTextDirection(style.textDirection)
val builder: ParagraphBuilder
var para: Paragraph
init {
@@ -61,7 +65,8 @@
textStyle = style,
spanStyles = spanStyles,
placeholders = placeholders,
- density = density
+ density = density,
+ textDirection = textDirection
)
para = builder.build()
@@ -70,4 +75,26 @@
override val minIntrinsicWidth = ceil(para.getMinIntrinsicWidth())
override val maxIntrinsicWidth = ceil(para.getMaxIntrinsicWidth())
+
+ private fun resolveTextDirection(direction: TextDirection?): ResolvedTextDirection {
+ return when (direction) {
+ TextDirection.Ltr -> ResolvedTextDirection.Ltr
+ TextDirection.Rtl -> ResolvedTextDirection.Rtl
+ TextDirection.Content -> contentBasedTextDirection() ?: ResolvedTextDirection.Ltr
+ TextDirection.ContentOrLtr -> contentBasedTextDirection() ?: ResolvedTextDirection.Ltr
+ TextDirection.ContentOrRtl -> contentBasedTextDirection() ?: ResolvedTextDirection.Rtl
+ null -> ResolvedTextDirection.Ltr
+ }
+ }
+
+ private fun contentBasedTextDirection(): ResolvedTextDirection? {
+ for (char in text) {
+ when (char.directionality) {
+ CharDirectionality.LEFT_TO_RIGHT -> return ResolvedTextDirection.Ltr
+ CharDirectionality.RIGHT_TO_LEFT -> return ResolvedTextDirection.Rtl
+ else -> continue
+ }
+ }
+ return null
+ }
}
diff --git a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
index e8d8c27..13af274 100644
--- a/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
+++ b/compose/ui/ui-text/src/desktopTest/kotlin/androidx/compose/ui/text/DesktopParagraphTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.FontLoader
import androidx.compose.ui.text.platform.Font
+import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
import com.google.common.truth.Truth
@@ -132,6 +133,91 @@
}
}
+ @Test
+ fun getHorizontalPositionForOffset_primary_Bidi_singleLine_textDirectionDefault() {
+ with(defaultDensity) {
+ val ltrText = "abc"
+ val rtlText = "\u05D0\u05D1\u05D2"
+ val text = ltrText + rtlText
+ val fontSize = 50.sp
+ val fontSizeInPx = fontSize.toPx()
+ val width = text.length * fontSizeInPx
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(fontSize = fontSize),
+ width = width
+ )
+
+ for (i in 0..ltrText.length) {
+ Truth.assertThat(paragraph.getHorizontalPosition(i, true))
+ .isEqualTo(fontSizeInPx * i)
+ }
+
+ for (i in 1 until rtlText.length) {
+ Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, true))
+ .isEqualTo(width - fontSizeInPx * i)
+ }
+ }
+ }
+
+ @Test
+ fun getHorizontalPositionForOffset_notPrimary_Bidi_singleLine_textDirectionLtr() {
+ with(defaultDensity) {
+ val ltrText = "abc"
+ val rtlText = "\u05D0\u05D1\u05D2"
+ val text = ltrText + rtlText
+ val fontSize = 50.sp
+ val fontSizeInPx = fontSize.toPx()
+ val width = text.length * fontSizeInPx
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontSize = fontSize,
+ textDirection = TextDirection.Ltr
+ ),
+ width = width
+ )
+
+ for (i in ltrText.indices) {
+ Truth.assertThat(paragraph.getHorizontalPosition(i, false))
+ .isEqualTo(fontSizeInPx * i)
+ }
+
+ for (i in rtlText.indices) {
+ Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, false))
+ .isEqualTo(width - fontSizeInPx * i)
+ }
+
+ Truth.assertThat(paragraph.getHorizontalPosition(text.length, false))
+ .isEqualTo(width - rtlText.length * fontSizeInPx)
+ }
+ }
+
+ @Test
+ fun getWordBoundary_spaces() {
+ val text = "ab cd e"
+ val paragraph = simpleParagraph(
+ text = text,
+ style = TextStyle(
+ fontFamily = fontFamilyMeasureFont,
+ fontSize = 20.sp
+ )
+ )
+
+ val singleSpaceStartResult = paragraph.getWordBoundary(text.indexOf('b') + 1)
+ Truth.assertThat(singleSpaceStartResult.start).isEqualTo(text.indexOf('a'))
+ Truth.assertThat(singleSpaceStartResult.end).isEqualTo(text.indexOf('b') + 1)
+
+ val singleSpaceEndResult = paragraph.getWordBoundary(text.indexOf('c'))
+
+ Truth.assertThat(singleSpaceEndResult.start).isEqualTo(text.indexOf('c'))
+ Truth.assertThat(singleSpaceEndResult.end).isEqualTo(text.indexOf('d') + 1)
+
+ val doubleSpaceResult = paragraph.getWordBoundary(text.indexOf('d') + 2)
+ Truth.assertThat(doubleSpaceResult.start).isEqualTo(text.indexOf('d') + 2)
+ Truth.assertThat(doubleSpaceResult.end).isEqualTo(text.indexOf('d') + 2)
+ }
+
private fun simpleParagraph(
text: String = "",
style: TextStyle? = null,
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
index f89d206..98e3857c 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Constraints.kt
@@ -186,7 +186,7 @@
companion object {
/**
* A value that [maxWidth] or [maxHeight] will be set to when the constraint should
- * be considered infinite. [hasBoundedHeight] or [hasBoundedWidth] will be
+ * be considered infinite. [hasBoundedWidth] or [hasBoundedHeight] will be
* `false` when [maxWidth] or [maxHeight] is [Infinity], respectively.
*/
const val Infinity = Int.MAX_VALUE
diff --git a/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/1.0.0-beta04.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/current.ignore b/compose/ui/ui-viewbinding/api/current.ignore
new file mode 100644
index 0000000..0b8fcf4
--- /dev/null
+++ b/compose/ui/ui-viewbinding/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.compose.ui.viewinterop.databinding:
+ Removed package androidx.compose.ui.viewinterop.databinding
diff --git a/compose/ui/ui-viewbinding/api/current.txt b/compose/ui/ui-viewbinding/api/current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/current.txt
+++ b/compose/ui/ui-viewbinding/api/current.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/public_plus_experimental_1.0.0-beta04.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt b/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-viewbinding/api/public_plus_experimental_current.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt b/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui-viewbinding/api/restricted_1.0.0-beta04.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/api/restricted_current.ignore b/compose/ui/ui-viewbinding/api/restricted_current.ignore
new file mode 100644
index 0000000..0b8fcf4
--- /dev/null
+++ b/compose/ui/ui-viewbinding/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.compose.ui.viewinterop.databinding:
+ Removed package androidx.compose.ui.viewinterop.databinding
diff --git a/compose/ui/ui-viewbinding/api/restricted_current.txt b/compose/ui/ui-viewbinding/api/restricted_current.txt
index 742107f..07ba909 100644
--- a/compose/ui/ui-viewbinding/api/restricted_current.txt
+++ b/compose/ui/ui-viewbinding/api/restricted_current.txt
@@ -7,15 +7,3 @@
}
-package androidx.compose.ui.viewinterop.databinding {
-
- public final class TestLayoutBinding implements androidx.viewbinding.ViewBinding {
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding bind(android.view.View);
- method public android.widget.LinearLayout getRoot();
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater);
- method public static androidx.compose.ui.viewinterop.databinding.TestLayoutBinding inflate(android.view.LayoutInflater, android.view.ViewGroup?, boolean);
- field public final android.widget.FrameLayout second;
- }
-
-}
-
diff --git a/compose/ui/ui-viewbinding/build.gradle b/compose/ui/ui-viewbinding/build.gradle
index 9a0d080..046b10d 100644
--- a/compose/ui/ui-viewbinding/build.gradle
+++ b/compose/ui/ui-viewbinding/build.gradle
@@ -31,6 +31,11 @@
implementation(KOTLIN_STDLIB)
implementation(project(":compose:ui:ui"))
+ implementation(project(":compose:ui:ui-util"))
+ implementation(VIEW_BINDING)
+ // Required to ensure that Fragments inflated by AndroidViewBinding
+ // actually appear after configuration changes
+ implementation("androidx.fragment:fragment-ktx:1.3.2")
androidTestImplementation(project(":compose:foundation:foundation"))
androidTestImplementation(project(":compose:test-utils"))
@@ -47,9 +52,3 @@
description = "Compose integration with ViewBinding"
legacyDisableKotlinStrictApiMode = true
}
-
-android {
- buildFeatures {
- viewBinding true
- }
-}
diff --git a/compose/ui/ui-viewbinding/samples/build.gradle b/compose/ui/ui-viewbinding/samples/build.gradle
index 93545c3..a73c3a1 100644
--- a/compose/ui/ui-viewbinding/samples/build.gradle
+++ b/compose/ui/ui-viewbinding/samples/build.gradle
@@ -34,6 +34,16 @@
implementation(project(":compose:runtime:runtime"))
implementation(project(":compose:ui:ui"))
implementation(project(":compose:ui:ui-viewbinding"))
+ // Used when creating layouts that contain a FragmentContainerView
+ implementation("androidx.fragment:fragment-ktx:1.3.2")
+
+ androidTestImplementation(project(":compose:foundation:foundation"))
+ androidTestImplementation(project(":compose:test-utils"))
+ androidTestImplementation(project(":activity:activity-compose"))
+ androidTestImplementation(project(":internal-testutils-runtime"))
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(JUNIT)
+ androidTestImplementation(TRUTH)
}
androidx {
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml b/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..d6b8107
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.compose.ui.viewbinding.samples">
+ <application>
+ <activity android:name="androidx.compose.ui.samples.InflatedFragmentActivity"/>
+ <activity android:name="androidx.compose.ui.samples.ChildInflatedFragmentActivity"/>
+ <activity android:name="androidx.compose.ui.samples.EmptyFragmentActivity"/>
+ </application>
+</manifest>
diff --git a/compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
similarity index 89%
rename from compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt
rename to compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
index 327a41f..f1b25c9 100644
--- a/compose/ui/ui-viewbinding/src/androidTest/java/androidx/compose/ui/viewinterop/AndroidViewBindingTest.kt
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/AndroidViewBindingTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.compose.ui.viewinterop
+package androidx.compose.ui.samples
import android.os.Build
import androidx.compose.foundation.layout.requiredSize
@@ -33,7 +33,8 @@
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.databinding.TestLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.compose.ui.viewbinding.samples.databinding.SampleLayoutBinding
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.MediumTest
@@ -54,7 +55,7 @@
@Test
fun drawing() {
rule.setContent {
- AndroidViewBinding(TestLayoutBinding::inflate, Modifier.testTag("layout"))
+ AndroidViewBinding(SampleLayoutBinding::inflate, Modifier.testTag("layout"))
}
val size = 50.dp
@@ -69,7 +70,7 @@
fun update() {
val color = mutableStateOf(Color.Gray)
rule.setContent {
- AndroidViewBinding(TestLayoutBinding::inflate, Modifier.testTag("layout")) {
+ AndroidViewBinding(SampleLayoutBinding::inflate, Modifier.testTag("layout")) {
second.setBackgroundColor(color.value.toArgb())
}
}
@@ -96,7 +97,7 @@
val sizeIpx = with(density) { size.roundToPx() }
CompositionLocalProvider(LocalDensity provides density) {
AndroidViewBinding(
- TestLayoutBinding::inflate,
+ SampleLayoutBinding::inflate,
Modifier.requiredSize(size).onGloballyPositioned {
Truth.assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
}
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt
new file mode 100644
index 0000000..8dea4a0
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRecreateTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.samples
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.activity.compose.setContent
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.compose.ui.viewbinding.samples.databinding.TestFragmentLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.add
+import androidx.fragment.app.commit
+import androidx.test.core.app.ActivityScenario
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val PARENT_FRAGMENT_CONTAINER_ID = 1
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FragmentRecreateTest {
+
+ @Test
+ fun testRecreateFragment() {
+ with(ActivityScenario.launch(InflatedFragmentActivity::class.java)) {
+ val fragment = withActivity {
+ supportFragmentManager.findFragmentById(R.id.fragment_container)!!
+ }
+ assertThat(fragment.requireView().parent).isNotNull()
+
+ recreate()
+
+ val recreatedFragment = withActivity {
+ supportFragmentManager.findFragmentById(R.id.fragment_container)!!
+ }
+ assertThat(recreatedFragment.requireView().parent).isNotNull()
+ }
+ }
+
+ @Test
+ fun testRecreateChildFragment() {
+ with(ActivityScenario.launch(ChildInflatedFragmentActivity::class.java)) {
+ val parentFragment = withActivity {
+ supportFragmentManager.findFragmentById(PARENT_FRAGMENT_CONTAINER_ID)!!
+ }
+ val fragment = parentFragment.childFragmentManager
+ .findFragmentById(R.id.fragment_container)
+ assertWithMessage("Fragment should be added as a child fragment")
+ .that(fragment).isNotNull()
+ assertThat(fragment!!.requireView().parent).isNotNull()
+
+ recreate()
+
+ val recreatedParentFragment = withActivity {
+ supportFragmentManager.findFragmentById(PARENT_FRAGMENT_CONTAINER_ID)!!
+ }
+ val recreatedFragment = recreatedParentFragment.childFragmentManager
+ .findFragmentById(R.id.fragment_container)
+ assertWithMessage("Fragment should be added as a child fragment")
+ .that(recreatedFragment).isNotNull()
+ assertThat(recreatedFragment!!.requireView().parent).isNotNull()
+ }
+ }
+}
+
+class InflatedFragmentActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+ }
+ }
+}
+
+class ChildInflatedFragmentActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(
+ FragmentContainerView(this).apply {
+ id = PARENT_FRAGMENT_CONTAINER_ID
+ }
+ )
+ if (savedInstanceState == null) {
+ supportFragmentManager.commit {
+ setReorderingAllowed(true)
+ add<ParentFragment>(PARENT_FRAGMENT_CONTAINER_ID)
+ }
+ }
+ }
+
+ class ParentFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ) = ComposeView(requireContext()).apply {
+ setContent {
+ AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
new file mode 100644
index 0000000..1255dbd
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/FragmentRemoveTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.samples
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.compose.ui.viewbinding.samples.databinding.TestFragmentLayoutBinding
+import androidx.compose.ui.viewinterop.AndroidViewBinding
+import androidx.fragment.app.FragmentActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class FragmentRemoveTest {
+
+ @get:Rule
+ val rule = createAndroidComposeRule<InflatedFragmentActivity>()
+
+ @Test
+ fun testRemoval() {
+ var show by mutableStateOf(true)
+
+ rule.setContent {
+ if (show) {
+ AndroidViewBinding(TestFragmentLayoutBinding::inflate)
+ }
+ }
+
+ var fragment = rule.activity.supportFragmentManager
+ .findFragmentById(R.id.fragment_container)
+ assertWithMessage("Fragment should be present when AndroidViewBinding is in the hierarchy")
+ .that(fragment)
+ .isNotNull()
+
+ show = false
+
+ rule.waitForIdle()
+
+ fragment = rule.activity.supportFragmentManager
+ .findFragmentById(R.id.fragment_container)
+ assertWithMessage("Fragment should be removed when the AndroidViewBinding is removed")
+ .that(fragment)
+ .isNull()
+ }
+}
+
+class EmptyFragmentActivity : FragmentActivity()
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
similarity index 76%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
index 5ece3c2..e94e7fc 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/compose/ui/ui-viewbinding/samples/src/androidTest/java/androidx/compose/ui/samples/InflatedFragment.kt
@@ -14,8 +14,9 @@
* limitations under the License.
*/
-package androidx.compose.material.catalog
+package androidx.compose.ui.samples
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.compose.ui.viewbinding.samples.R
+import androidx.fragment.app.Fragment
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+class InflatedFragment : Fragment(R.layout.sample_layout)
diff --git a/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml b/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
index 2709cbb..f27fc71 100644
--- a/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-viewbinding/samples/src/main/AndroidManifest.xml
@@ -14,4 +14,4 @@
limitations under the License.
-->
-<manifest package="androidx.compose.ui.viewinterop.samples" />
+<manifest package="androidx.compose.ui.viewbinding.samples" />
diff --git a/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt b/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
index 0449083..1555523 100644
--- a/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
+++ b/compose/ui/ui-viewbinding/samples/src/main/java/androidx/compose/ui/samples/AndroidViewBindingSample.kt
@@ -19,8 +19,8 @@
import android.graphics.Color
import androidx.annotation.Sampled
import androidx.compose.runtime.Composable
+import androidx.compose.ui.viewbinding.samples.databinding.SampleLayoutBinding
import androidx.compose.ui.viewinterop.AndroidViewBinding
-import androidx.compose.ui.viewinterop.samples.databinding.SampleLayoutBinding
@Sampled
@Composable
diff --git a/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml b/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml
new file mode 100644
index 0000000..0806f66
--- /dev/null
+++ b/compose/ui/ui-viewbinding/samples/src/main/res/layout/test_fragment_layout.xml
@@ -0,0 +1,22 @@
+<?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.
+ -->
+
+<androidx.fragment.app.FragmentContainerView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/fragment_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:name="androidx.compose.ui.samples.InflatedFragment"/>
\ No newline at end of file
diff --git a/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml b/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
index 6e4f4af..4cfbcc0 100644
--- a/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
+++ b/compose/ui/ui-viewbinding/src/main/AndroidManifest.xml
@@ -14,4 +14,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest package="androidx.compose.ui.viewinterop" />
+<manifest package="androidx.compose.ui.viewbinding" />
diff --git a/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt b/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
index 55eaac8..145df82 100644
--- a/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
+++ b/compose/ui/ui-viewbinding/src/main/java/androidx/compose/ui/viewinterop/AndroidViewBinding.kt
@@ -22,9 +22,20 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.Ref
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.util.fastForEach
+import androidx.core.view.forEach
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentContainerView
+import androidx.fragment.app.commit
+import androidx.fragment.app.findFragment
import androidx.viewbinding.ViewBinding
/**
@@ -51,11 +62,32 @@
update: T.() -> Unit = {}
) {
val viewBindingRef = remember { Ref<T>() }
- val viewBlock: (Context) -> View = remember {
+ val localView = LocalView.current
+ // Find the parent fragment, if one exists. This will let us ensure that
+ // fragments inflated via a FragmentContainerView are properly nested
+ // (which, in turn, allows the fragments to properly save/restore their state)
+ val parentFragment = remember(localView) {
+ try {
+ localView.findFragment<Fragment>()
+ } catch (e: IllegalStateException) {
+ // findFragment throws if no parent fragment is found
+ null
+ }
+ }
+ val fragmentContainerViews = remember { mutableStateListOf<FragmentContainerView>() }
+ val viewBlock: (Context) -> View = remember(localView) {
{ context ->
- val inflater = LayoutInflater.from(context)
+ // Inflated fragments are automatically nested properly when
+ // using the parent fragment's LayoutInflater
+ val inflater = parentFragment?.layoutInflater ?: LayoutInflater.from(context)
val viewBinding = factory(inflater, FrameLayout(context), false)
viewBindingRef.value = viewBinding
+ // Find all FragmentContainerView instances in the newly inflated layout
+ fragmentContainerViews.clear()
+ val rootGroup = viewBinding.root as? ViewGroup
+ if (rootGroup != null) {
+ findFragmentContainerViews(rootGroup, fragmentContainerViews)
+ }
viewBinding.root
}
}
@@ -64,4 +96,41 @@
modifier = modifier,
update = { viewBindingRef.value?.update() }
)
+
+ // Set up a DisposableEffect for each FragmentContainerView that will
+ // clean up inflated fragments when the AndroidViewBinding is disposed
+ val localContext = LocalContext.current
+ fragmentContainerViews.fastForEach { container ->
+ DisposableEffect(localContext, container) {
+ // Find the right FragmentManager
+ val fragmentManager = parentFragment?.childFragmentManager
+ ?: (localContext as? FragmentActivity)?.supportFragmentManager
+ // Now find the fragment inflated via the FragmentContainerView
+ val existingFragment = fragmentManager?.findFragmentById(container.id)
+ onDispose {
+ if (existingFragment != null && !fragmentManager.isStateSaved) {
+ // If the state isn't saved, that means that some state change
+ // has removed this Composable from the hierarchy
+ fragmentManager.commit {
+ remove(existingFragment)
+ }
+ }
+ }
+ }
+ }
+}
+
+private fun findFragmentContainerViews(
+ viewGroup: ViewGroup,
+ list: MutableList<FragmentContainerView>
+) {
+ if (viewGroup is FragmentContainerView) {
+ list += viewGroup
+ } else {
+ viewGroup.forEach {
+ if (it is ViewGroup) {
+ findFragmentContainerViews(it, list)
+ }
+ }
+ }
}
diff --git a/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml b/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml
deleted file mode 100644
index 85ba5ee..0000000
--- a/compose/ui/ui-viewbinding/src/main/res/layout/test_layout.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright 2020 The Android Open Source Project
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:orientation="vertical"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content">
- <FrameLayout
- android:layout_width="50dp"
- android:layout_height="50dp"
- android:background="#0000FF" />
- <FrameLayout
- android:id="@+id/second"
- android:layout_width="50dp"
- android:layout_height="50dp"
- android:background="#000000" />
-</LinearLayout>
\ No newline at end of file
diff --git a/compose/ui/ui/api/1.0.0-beta04.txt b/compose/ui/ui/api/1.0.0-beta04.txt
index 27cc180..a926e4a 100644
--- a/compose/ui/ui/api/1.0.0-beta04.txt
+++ b/compose/ui/ui/api/1.0.0-beta04.txt
@@ -346,6 +346,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2071,11 +2074,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2088,16 +2091,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 27cc180..a926e4a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -346,6 +346,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2071,11 +2074,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2088,16 +2091,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
index 223598a..76230b4 100644
--- a/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
+++ b/compose/ui/ui/api/public_plus_experimental_1.0.0-beta04.txt
@@ -438,6 +438,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2048,13 +2051,16 @@
@androidx.compose.ui.ExperimentalComposeUiApi public final class LocalSoftwareKeyboardController {
method @androidx.compose.runtime.Composable public androidx.compose.ui.platform.SoftwareKeyboardController? getCurrent();
+ method public infix androidx.compose.runtime.ProvidedValue<androidx.compose.ui.platform.SoftwareKeyboardController> provides(androidx.compose.ui.platform.SoftwareKeyboardController softwareKeyboardController);
property @androidx.compose.runtime.Composable public final androidx.compose.ui.platform.SoftwareKeyboardController? current;
field public static final androidx.compose.ui.platform.LocalSoftwareKeyboardController INSTANCE;
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
- method public void hideSoftwareKeyboard();
- method public void showSoftwareKeyboard();
+ @androidx.compose.runtime.Stable @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
+ method public void hide();
+ method @Deprecated public default void hideSoftwareKeyboard();
+ method public void show();
+ method @Deprecated public default void showSoftwareKeyboard();
}
public final class TestTagKt {
@@ -2204,11 +2210,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2221,16 +2227,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 223598a..76230b4 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -438,6 +438,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2048,13 +2051,16 @@
@androidx.compose.ui.ExperimentalComposeUiApi public final class LocalSoftwareKeyboardController {
method @androidx.compose.runtime.Composable public androidx.compose.ui.platform.SoftwareKeyboardController? getCurrent();
+ method public infix androidx.compose.runtime.ProvidedValue<androidx.compose.ui.platform.SoftwareKeyboardController> provides(androidx.compose.ui.platform.SoftwareKeyboardController softwareKeyboardController);
property @androidx.compose.runtime.Composable public final androidx.compose.ui.platform.SoftwareKeyboardController? current;
field public static final androidx.compose.ui.platform.LocalSoftwareKeyboardController INSTANCE;
}
- @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
- method public void hideSoftwareKeyboard();
- method public void showSoftwareKeyboard();
+ @androidx.compose.runtime.Stable @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftwareKeyboardController {
+ method public void hide();
+ method @Deprecated public default void hideSoftwareKeyboard();
+ method public void show();
+ method @Deprecated public default void showSoftwareKeyboard();
}
public final class TestTagKt {
@@ -2204,11 +2210,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2221,16 +2227,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/restricted_1.0.0-beta04.txt b/compose/ui/ui/api/restricted_1.0.0-beta04.txt
index a43f540..8864bcd 100644
--- a/compose/ui/ui/api/restricted_1.0.0-beta04.txt
+++ b/compose/ui/ui/api/restricted_1.0.0-beta04.txt
@@ -346,6 +346,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2101,11 +2104,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2118,16 +2121,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index a43f540..8864bcd 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -346,6 +346,9 @@
public final class FocusTraversalKt {
}
+ public final class TwoDimensionalFocusSearchKt {
+ }
+
}
package androidx.compose.ui.graphics {
@@ -2101,11 +2104,11 @@
}
public final class ColorResources_androidKt {
- method @androidx.compose.runtime.Composable public static long colorResource(@ColorRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static long colorResource(@ColorRes int id);
}
public final class FontResources_androidKt {
- method @androidx.compose.runtime.Composable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static androidx.compose.ui.text.font.Typeface fontResource(androidx.compose.ui.text.font.FontFamily fontFamily);
}
public final class ImageResources_androidKt {
@@ -2118,16 +2121,16 @@
}
public final class PrimitiveResources_androidKt {
- method @androidx.compose.runtime.Composable public static boolean booleanResource(@BoolRes int id);
- method @androidx.compose.runtime.Composable public static float dimensionResource(@DimenRes int id);
- method @androidx.compose.runtime.Composable public static int[] integerArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static int integerResource(@IntegerRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static boolean booleanResource(@BoolRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static float dimensionResource(@DimenRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int[] integerArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static int integerResource(@IntegerRes int id);
}
public final class StringResources_androidKt {
- method @androidx.compose.runtime.Composable public static String![] stringArrayResource(@ArrayRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id);
- method @androidx.compose.runtime.Composable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String![] stringArrayResource(@ArrayRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id);
+ method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public static String stringResource(@StringRes int id, java.lang.Object... formatArgs);
}
public final class VectorResources_androidKt {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
index ea2650c..e88deb2 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/SoftwareKeyboardControllerDemo.kt
@@ -63,7 +63,7 @@
Button(
onClick = {
isHidden = true
- keyboardController?.hideSoftwareKeyboard()
+ keyboardController?.hide()
},
enabled = !isHidden,
modifier = Modifier.padding(vertical = 8.dp)
@@ -74,7 +74,7 @@
onClick = {
isHidden = false
focusRequester.requestFocus()
- keyboardController?.showSoftwareKeyboard()
+ keyboardController?.show()
},
enabled = isHidden
) {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index 645f31e..6912493 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -17,9 +17,17 @@
package androidx.compose.ui.demos
import androidx.compose.foundation.demos.text.SoftwareKeyboardControllerDemo
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.integration.demos.common.DemoCategory
import androidx.compose.ui.demos.autofill.ExplicitAutofillTypesDemo
+import androidx.compose.ui.demos.focus.CustomFocusOrderDemo
+import androidx.compose.ui.demos.focus.FocusInDialogDemo
+import androidx.compose.ui.demos.focus.FocusInPopupDemo
+import androidx.compose.ui.demos.focus.FocusManagerMoveFocusDemo
+import androidx.compose.ui.demos.focus.FocusSearchDemo
import androidx.compose.ui.demos.focus.FocusableDemo
import androidx.compose.ui.demos.focus.ReuseFocusRequesterDemo
+import androidx.compose.ui.demos.gestures.DetectTapGesturesDemo
import androidx.compose.ui.demos.gestures.DoubleTapGestureFilterDemo
import androidx.compose.ui.demos.gestures.DoubleTapInTapDemo
import androidx.compose.ui.demos.gestures.DragAndScaleGestureFilterDemo
@@ -30,6 +38,7 @@
import androidx.compose.ui.demos.gestures.LongPressGestureDetectorDemo
import androidx.compose.ui.demos.gestures.NestedLongPressDemo
import androidx.compose.ui.demos.gestures.NestedPressingDemo
+import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
import androidx.compose.ui.demos.gestures.NestedScrollingDemo
import androidx.compose.ui.demos.gestures.PointerInputDuringSubComp
import androidx.compose.ui.demos.gestures.PopupDragDemo
@@ -37,17 +46,9 @@
import androidx.compose.ui.demos.gestures.RawDragGestureFilterDemo
import androidx.compose.ui.demos.gestures.ScaleGestureFilterDemo
import androidx.compose.ui.demos.gestures.ScrollGestureFilterDemo
-import androidx.compose.ui.demos.gestures.DetectTapGesturesDemo
import androidx.compose.ui.demos.gestures.VerticalScrollerInDrawerDemo
import androidx.compose.ui.demos.keyinput.KeyInputDemo
import androidx.compose.ui.demos.viewinterop.ViewInteropDemo
-import androidx.compose.integration.demos.common.ComposableDemo
-import androidx.compose.integration.demos.common.DemoCategory
-import androidx.compose.ui.demos.focus.CustomFocusOrderDemo
-import androidx.compose.ui.demos.focus.FocusInDialogDemo
-import androidx.compose.ui.demos.focus.FocusInPopupDemo
-import androidx.compose.ui.demos.focus.FocusManagerMoveFocusDemo
-import androidx.compose.ui.demos.gestures.NestedScrollDispatchDemo
import androidx.compose.ui.samples.NestedScrollConnectionSample
private val GestureDemos = DemoCategory(
@@ -108,6 +109,7 @@
ComposableDemo("Focus Within Dialog") { FocusInDialogDemo() },
ComposableDemo("Focus Within Popup") { FocusInPopupDemo() },
ComposableDemo("Reuse Focus Requester") { ReuseFocusRequesterDemo() },
+ ComposableDemo("Focus Search") { FocusSearchDemo() },
ComposableDemo("Custom Focus Order") { CustomFocusOrderDemo() },
ComposableDemo("FocusManager.moveFocus()") { FocusManagerMoveFocusDemo() }
)
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt
new file mode 100644
index 0000000..4d26bd2
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusSearchDemo.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.demos.focus
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.isFocused
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color.Companion.Black
+import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun FocusSearchDemo() {
+ Column {
+ Text("Use Tab/Shift+Tab or arrow keys to move focus:")
+ Box(Modifier.padding(10.dp).size(330.dp, 280.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 10.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(60.dp, 10.dp).size(40.dp, 40.dp))
+ Row(Modifier.offset(110.dp, 10.dp)) {
+ FocusableBox(Modifier.size(23.dp, 20.dp))
+ FocusableBox(Modifier.padding(horizontal = 10.dp).size(23.dp, 20.dp))
+ FocusableBox(Modifier.size(23.dp, 20.dp))
+ }
+ FocusableBox(Modifier.offset(210.dp, 10.dp).size(40.dp, 120.dp))
+ FocusableBox(Modifier.offset(260.dp, 10.dp).size(60.dp, 260.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 10.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(10.dp, 60.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(10.dp, 110.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(10.dp, 160.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(10.dp, 210.dp).size(40.dp, 40.dp))
+ }
+ FocusableBox(Modifier.offset(60.dp, 60.dp).size(18.dp, 18.dp))
+ FocusableBox(Modifier.offset(82.dp, 60.dp).size(18.dp, 18.dp))
+ FocusableBox(Modifier.offset(110.dp, 40.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(160.dp, 40.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(10.dp, 60.dp).size(40.dp, 210.dp))
+ FocusableBox(Modifier.offset(60.dp, 90.dp).size(140.dp, 40.dp))
+ FocusableBox(Modifier.offset(60.dp, 140.dp).size(190.dp, 130.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 10.dp).size(170.dp, 110.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 10.dp).size(150.dp, 90.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 10.dp).size(130.dp, 70.dp)) {
+ FocusableBox(Modifier.offset(10.dp, 15.dp).size(40.dp, 40.dp))
+ FocusableBox(Modifier.offset(58.dp, 15.dp).size(15.dp, 15.dp))
+ FocusableBox(Modifier.offset(58.dp, 40.dp).size(15.dp, 15.dp))
+ FocusableBox(Modifier.offset(80.dp, 15.dp).size(40.dp, 40.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun FocusableBox(
+ modifier: Modifier = Modifier,
+ content: @Composable BoxScope.() -> Unit = {}
+) {
+ var borderColor by remember { mutableStateOf(Black) }
+ Box(
+ modifier = modifier
+ .onFocusChanged { borderColor = if (it.isFocused) Red else Black }
+ .border(2.dp, borderColor)
+ .focusable(),
+ content = content
+ )
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
index dd27b86..49313dc 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/SoftwareKeyboardControllerSample.kt
@@ -55,7 +55,7 @@
setText,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
- onDone = { keyboardController?.hideSoftwareKeyboard() }
+ onDone = { keyboardController?.hide() }
),
modifier = Modifier
.focusRequester(focusRequester)
@@ -65,7 +65,7 @@
Button(
onClick = {
focusRequester.requestFocus()
- keyboardController?.showSoftwareKeyboard()
+ keyboardController?.show()
},
modifier = Modifier.fillMaxWidth()
) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index adfa884..837f607 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -20,6 +20,11 @@
import android.graphics.RectF
import android.os.Build
import android.os.Bundle
+import android.view.InputDevice
+import android.view.MotionEvent
+import android.view.MotionEvent.ACTION_HOVER_ENTER
+import android.view.MotionEvent.ACTION_HOVER_MOVE
+import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
@@ -29,7 +34,9 @@
import android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_SELECTION
import android.view.accessibility.AccessibilityNodeProvider
import android.view.accessibility.AccessibilityRecord
-import androidx.activity.ComponentActivity
+import android.widget.Button
+import android.widget.LinearLayout
+import android.widget.TextView
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -63,6 +70,7 @@
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsOff
@@ -79,6 +87,7 @@
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -96,6 +105,7 @@
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -107,13 +117,14 @@
import org.mockito.ArgumentMatcher
import org.mockito.ArgumentMatchers.any
import org.mockito.internal.matchers.apachecommons.ReflectionEquals
+import java.lang.reflect.Method
@LargeTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalFoundationApi::class)
class AndroidAccessibilityTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
private lateinit var androidComposeView: AndroidComposeView
private lateinit var container: OpenComposeView
@@ -872,24 +883,17 @@
}
}
- var rootNodeBoundsLeft = 0f
- var rootNodeBoundsTop = 0f
- rule.runOnIdle {
- val rootNode = androidComposeView.semanticsOwner.rootSemanticsNode
- rootNodeBoundsLeft = rootNode.boundsInWindow.left
- rootNodeBoundsTop = rootNode.boundsInWindow.top
- }
-
val toggleableNode = rule.onNodeWithTag(tag)
.fetchSemanticsNode("couldn't find node with tag $tag")
- val toggleableNodeBounds = toggleableNode.boundsInWindow
+ val toggleableNodeBounds = toggleableNode.boundsInRoot
- val toggleableNodeId = delegate.getVirtualViewAt(
- (toggleableNodeBounds.left + toggleableNodeBounds.right) / 2 - rootNodeBoundsLeft,
- (toggleableNodeBounds.top + toggleableNodeBounds.bottom) / 2 - rootNodeBoundsTop
+ val toggleableNodeFound = delegate.findSemanticsNodeAt(
+ (toggleableNodeBounds.left + toggleableNodeBounds.right) / 2,
+ (toggleableNodeBounds.top + toggleableNodeBounds.bottom) / 2,
+ androidComposeView.semanticsOwner.rootSemanticsNode
)
-
- assertEquals(toggleableNode.id, toggleableNodeId)
+ assertNotNull(toggleableNodeFound)
+ assertEquals(toggleableNode.id, toggleableNodeFound!!.id)
}
@Test
@@ -914,28 +918,187 @@
}
}
- var rootNodeBoundsLeft = 0f
- var rootNodeBoundsTop = 0f
- rule.runOnIdle {
- val rootNode = androidComposeView.semanticsOwner.rootSemanticsNode
- rootNodeBoundsLeft = rootNode.boundsInWindow.left
- rootNodeBoundsTop = rootNode.boundsInWindow.top
- }
-
val overlappedChildOneNode = rule.onNodeWithTag(childOneTag)
.fetchSemanticsNode("couldn't find node with tag $childOneTag")
val overlappedChildTwoNode = rule.onNodeWithTag(childTwoTag)
.fetchSemanticsNode("couldn't find node with tag $childTwoTag")
- val overlappedChildNodeBounds = overlappedChildTwoNode.boundsInWindow
- val overlappedChildNodeId = delegate.getVirtualViewAt(
- (overlappedChildNodeBounds.left + overlappedChildNodeBounds.right) / 2 -
- rootNodeBoundsLeft,
- (overlappedChildNodeBounds.top + overlappedChildNodeBounds.bottom) / 2 -
- rootNodeBoundsTop
+ val overlappedChildNodeBounds = overlappedChildTwoNode.boundsInRoot
+ val overlappedChildNode = delegate.findSemanticsNodeAt(
+ (overlappedChildNodeBounds.left + overlappedChildNodeBounds.right) / 2,
+ (overlappedChildNodeBounds.top + overlappedChildNodeBounds.bottom) / 2,
+ androidComposeView.semanticsOwner.rootSemanticsNode
+ )
+ assertNotNull(overlappedChildNode)
+ assertEquals(overlappedChildOneNode.id, overlappedChildNode!!.id)
+ assertNotEquals(overlappedChildTwoNode.id, overlappedChildNode.id)
+ }
+
+ @Test
+ @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.P)
+ fun testViewInterop_findViewByAccessibilityId() {
+ val androidViewTag = "androidView"
+ container.setContent {
+ Column {
+ AndroidView(
+ { context ->
+ LinearLayout(context).apply {
+ addView(TextView(context).apply { text = "Text1" })
+ addView(TextView(context).apply { text = "Text2" })
+ }
+ },
+ Modifier.testTag(androidViewTag)
+ )
+ BasicText("text")
+ }
+ }
+
+ val getViewRootImplMethod = View::class.java.getDeclaredMethod("getViewRootImpl")
+ getViewRootImplMethod.isAccessible = true
+ val rootView = getViewRootImplMethod.invoke(container)
+
+ val forName = Class::class.java.getMethod("forName", String::class.java)
+ val getDeclaredMethod = Class::class.java.getMethod(
+ "getDeclaredMethod",
+ String::class.java,
+ arrayOf<Class<*>>()::class.java
)
- assertEquals(overlappedChildOneNode.id, overlappedChildNodeId)
- assertNotEquals(overlappedChildTwoNode.id, overlappedChildNodeId)
+ val viewRootImplClass = forName.invoke(null, "android.view.ViewRootImpl") as Class<*>
+ val getAccessibilityInteractionControllerMethod = getDeclaredMethod.invoke(
+ viewRootImplClass,
+ "getAccessibilityInteractionController",
+ arrayOf<Class<*>>()
+ ) as Method
+ getAccessibilityInteractionControllerMethod.isAccessible = true
+ val accessibilityInteractionController =
+ getAccessibilityInteractionControllerMethod.invoke(rootView)
+
+ val accessibilityInteractionControllerClass =
+ forName.invoke(null, "android.view.AccessibilityInteractionController") as Class<*>
+ val findViewByAccessibilityIdMethod =
+ getDeclaredMethod.invoke(
+ accessibilityInteractionControllerClass,
+ "findViewByAccessibilityId",
+ arrayOf<Class<*>>(Int::class.java)
+ ) as Method
+ findViewByAccessibilityIdMethod.isAccessible = true
+
+ val androidView = rule.onNodeWithTag(androidViewTag)
+ .fetchSemanticsNode("can't find node with tag $androidViewTag")
+ val viewGroup = androidComposeView.androidViewsHandler
+ .layoutNodeToHolder[androidView.layoutNode]!!.view as ViewGroup
+ val getAccessibilityViewIdMethod = View::class.java
+ .getDeclaredMethod("getAccessibilityViewId")
+ getAccessibilityViewIdMethod.isAccessible = true
+
+ val textTwo = viewGroup.getChildAt(1)
+ val textViewTwoId = getAccessibilityViewIdMethod.invoke(textTwo)
+ val foundView = findViewByAccessibilityIdMethod.invoke(
+ accessibilityInteractionController,
+ textViewTwoId
+ )
+ assertNotNull(foundView)
+ assertEquals(textTwo, foundView)
+ }
+
+ @Test
+ fun testViewInterop_viewChildExists() {
+ val colTag = "ColTag"
+ val buttonText = "button text"
+ container.setContent {
+ Column(Modifier.testTag(colTag)) {
+ AndroidView(::Button) {
+ it.text = buttonText
+ it.setOnClickListener {}
+ }
+ BasicText("text")
+ }
+ }
+
+ val colSemanticsNode = rule.onNodeWithTag(colTag)
+ .fetchSemanticsNode("can't find node with tag $colTag")
+ val colAccessibilityNode = provider.createAccessibilityNodeInfo(colSemanticsNode.id)
+ assertEquals(2, colAccessibilityNode.childCount)
+ assertEquals(2, colSemanticsNode.children.size)
+ val buttonHolder = androidComposeView.androidViewsHandler
+ .layoutNodeToHolder[colSemanticsNode.children[0].layoutNode]
+ assertNotNull(buttonHolder)
+ assertEquals(
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES,
+ buttonHolder!!.importantForAccessibility
+ )
+ assertEquals(buttonText, (buttonHolder.getChildAt(0) as Button).text)
+ }
+
+ @Test
+ fun testViewInterop_hoverEnterExit() {
+ val colTag = "ColTag"
+ val textTag = "TextTag"
+ val buttonText = "button text"
+ container.setContent {
+ Column(Modifier.testTag(colTag)) {
+ AndroidView(::Button) {
+ it.text = buttonText
+ it.setOnClickListener {}
+ }
+ BasicText(text = "text", modifier = Modifier.testTag(textTag))
+ }
+ }
+
+ val colSemanticsNode = rule.onNodeWithTag(colTag)
+ .fetchSemanticsNode("can't find node with tag $colTag")
+ rule.runOnUiThread {
+ val bounds = colSemanticsNode.children[0].boundsInRoot
+ val hoverEnter = MotionEvent.obtain(
+ 0 /* downTime */, 0 /* eventTime */,
+ ACTION_HOVER_ENTER, (bounds.left + bounds.right) / 2 /* x */,
+ (bounds.top + bounds.bottom) / 2/* y */, 0 /* metaState*/
+ )
+ hoverEnter.source = InputDevice.SOURCE_CLASS_POINTER
+ assertTrue(androidComposeView.dispatchHoverEvent(hoverEnter))
+ assertEquals(
+ AndroidComposeViewAccessibilityDelegateCompat.InvalidId,
+ delegate.hoveredVirtualViewId
+ )
+ }
+ rule.runOnIdle {
+ verify(container, times(1)).requestSendAccessibilityEvent(
+ eq(androidComposeView),
+ argThat(
+ ArgumentMatcher {
+ it.eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+ }
+ )
+ )
+ }
+
+ val textNode = rule.onNodeWithTag(textTag)
+ .fetchSemanticsNode("can't find node with tag $textTag")
+ rule.runOnUiThread {
+ val bounds = textNode.boundsInRoot
+ val hoverEnter = MotionEvent.obtain(
+ 0 /* downTime */, 0 /* eventTime */,
+ ACTION_HOVER_MOVE, (bounds.left + bounds.right) / 2 /* x */,
+ (bounds.top + bounds.bottom) / 2/* y */, 0 /* metaState*/
+ )
+ hoverEnter.source = InputDevice.SOURCE_CLASS_POINTER
+ assertTrue(androidComposeView.dispatchHoverEvent(hoverEnter))
+ assertEquals(
+ textNode.id,
+ delegate.hoveredVirtualViewId
+ )
+ }
+ // verify hover exit accessibility event is sent from the previously hovered view
+ rule.runOnIdle {
+ verify(container, times(1)).requestSendAccessibilityEvent(
+ eq(androidComposeView),
+ argThat(
+ ArgumentMatcher {
+ it.eventType == AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+ }
+ )
+ )
+ }
}
@Test
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 59e9c0d..74117be 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -22,7 +22,6 @@
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
@@ -44,37 +43,38 @@
import androidx.compose.ui.semantics.CustomAccessibilityAction
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.ProgressBarRangeInfo
+import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.ScrollAxisRange
import androidx.compose.ui.semantics.SemanticsModifierCore
import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.semantics.SemanticsPropertyReceiver
-import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
import androidx.compose.ui.semantics.SemanticsWrapper
import androidx.compose.ui.semantics.collapse
-import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.copyText
import androidx.compose.ui.semantics.customActions
import androidx.compose.ui.semantics.cutText
import androidx.compose.ui.semantics.disabled
-import androidx.compose.ui.semantics.stateDescription
-import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.dismiss
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.focused
import androidx.compose.ui.semantics.getTextLayoutResult
+import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.horizontalScrollAxisRange
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.pasteText
+import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.setProgress
import androidx.compose.ui.semantics.setSelection
import androidx.compose.ui.semantics.setText
+import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.text
import androidx.compose.ui.semantics.textSelectionRange
import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.text.AnnotatedString
@@ -107,7 +107,7 @@
@RunWith(AndroidJUnit4::class)
class AndroidComposeViewAccessibilityDelegateCompatTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
private lateinit var accessibilityDelegate: AndroidComposeViewAccessibilityDelegateCompat
private lateinit var container: ViewGroup
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
index 6c03937..380b9b7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/RecyclerViewIntegrationTest.kt
@@ -19,12 +19,12 @@
import android.content.Context
import android.os.Bundle
import android.view.ViewGroup
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.unit.Constraints
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.LinearLayoutManager
@@ -108,7 +108,7 @@
}
}
-class RecyclerViewActivity : ComponentActivity() {
+class RecyclerViewActivity : TestActivity() {
private lateinit var recyclerView: RecyclerView
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
new file mode 100644
index 0000000..384b376a
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/KeyEventToFocusDirectionTest.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.focus
+
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.nativeKeyCode
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.ACTION_DOWN as KeyDown
+import android.view.KeyEvent.META_SHIFT_ON as Shift
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyEventToFocusDirectionTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private lateinit var owner: Owner
+
+ @Before
+ fun setup() {
+ rule.setContent {
+ owner = LocalView.current as Owner
+ }
+ }
+
+ @Test
+ fun left() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionLeft.nativeKeyCode))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Left)
+ }
+
+ @Test
+ fun right() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionRight.nativeKeyCode))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Right)
+ }
+
+ @Test
+ fun up() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionUp.nativeKeyCode))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Up)
+ }
+
+ @Test
+ fun down() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.DirectionDown.nativeKeyCode))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Down)
+ }
+
+ @Test
+ fun tab_next() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(KeyDown, Key.Tab.nativeKeyCode))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Next)
+ }
+
+ @Test
+ fun shiftTab_previous() {
+ // Arrange.
+ val keyEvent = KeyEvent(AndroidKeyEvent(0L, 0L, KeyDown, Key.Tab.nativeKeyCode, 0, Shift))
+
+ // Act.
+ val focusDirection = owner.getFocusDirection(keyEvent)
+
+ // Assert.
+ Truth.assertThat(focusDirection).isEqualTo(FocusDirection.Previous)
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt
new file mode 100644
index 0000000..302d3dc
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalInitialFocus.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.focus
+
+import android.view.View
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@MediumTest
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalInitialFocus(private val focusDirection: FocusDirection) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun initParameters() = listOf(Left, Right, Up, Down)
+ }
+
+ @Test
+ fun initialFocus() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var focusManager: FocusManager
+ val isFocused = MutableList(4) { mutableStateOf(false) }
+ rule.setContent {
+ view = LocalView.current
+ focusManager = LocalFocusManager.current
+ Column {
+ Row {
+ FocusableBox(isFocused[0])
+ FocusableBox(isFocused[1])
+ }
+ Row {
+ FocusableBox(isFocused[2])
+ FocusableBox(isFocused[3])
+ }
+ }
+ }
+ rule.runOnIdle { view.requestFocus() }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> assertThat(isFocused.values).containsExactly(false, false, true, false)
+ Down -> assertThat(isFocused.values).containsExactly(true, false, false, false)
+ Left -> assertThat(isFocused.values).containsExactly(false, false, false, true)
+ Right -> assertThat(isFocused.values).containsExactly(true, false, false, false)
+ else -> error(invalid)
+ }
+ }
+ }
+
+ @Test
+ fun initialFocus_whenThereIsOnlyOneFocusable() {
+ // Arrange.
+ var isFocused = mutableStateOf(false)
+ lateinit var view: View
+ lateinit var focusManager: FocusManager
+ rule.setContent {
+ view = LocalView.current
+ focusManager = LocalFocusManager.current
+ FocusableBox(isFocused)
+ }
+ rule.runOnIdle { view.requestFocus() }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle { assertThat(isFocused.value).isTrue() }
+ }
+
+ @Test
+ fun doesNotCrash_whenThereIsNoFocusable() {
+ // Arrange.
+ lateinit var view: View
+ lateinit var focusManager: FocusManager
+ rule.setContent {
+ view = LocalView.current
+ focusManager = LocalFocusManager.current
+ BasicText("Hello")
+ }
+ rule.runOnIdle { view.requestFocus() }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+ }
+
+ @Test
+ fun initialFocus_notTriggeredIfActiveElementIsNotRoot() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ var isColumnFocused = false
+ val isFocused = MutableList(4) { mutableStateOf(false) }
+ val initialFocusRequester = FocusRequester()
+ rule.setContent {
+ focusManager = LocalFocusManager.current
+ Column(
+ Modifier
+ .focusRequester(initialFocusRequester)
+ .onFocusChanged { isColumnFocused = it.isFocused }
+ .focusModifier()
+ ) {
+ Row {
+ FocusableBox(isFocused[0])
+ FocusableBox(isFocused[1])
+ }
+ Row {
+ FocusableBox(isFocused[2])
+ FocusableBox(isFocused[3])
+ }
+ }
+ }
+ rule.runOnIdle { initialFocusRequester.requestFocus() }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(isColumnFocused).isTrue()
+ assertThat(isFocused.values).containsExactly(false, false, false, false)
+ }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun movesFocusAmongSiblingsDeepInTheFocusHierarchy() {
+ // Arrange.
+ lateinit var focusManager: FocusManager
+ val isFocused = MutableList(2) { mutableStateOf(false) }
+ val (item1, item2) = FocusRequester.createRefs()
+ val siblings = @Composable {
+ FocusableBox(isFocused = isFocused[0], focusRequester = item1)
+ FocusableBox(isFocused = isFocused[1], focusRequester = item2)
+ }
+ val initialFocusedItem = when (focusDirection) {
+ Up, Left -> item2
+ Down, Right -> item1
+ else -> error(invalid)
+ }
+ rule.setContent {
+ focusManager = LocalFocusManager.current
+ FocusableBox {
+ FocusableBox {
+ FocusableBox {
+ FocusableBox {
+ FocusableBox {
+ FocusableBox {
+ when (focusDirection) {
+ Up, Down -> Column { siblings() }
+ Left, Right -> Row { siblings() }
+ else -> error(invalid)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ rule.runOnIdle { initialFocusedItem.requestFocus() }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Left -> assertThat(isFocused.values).containsExactly(true, false)
+ Down, Right -> assertThat(isFocused.values).containsExactly(false, true)
+ else -> error(invalid)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FocusableBox(
+ isFocused: MutableState<Boolean>? = null,
+ x: Int = 0,
+ y: Int = 0,
+ width: Int = 10,
+ height: Int = 10,
+ focusRequester: FocusRequester? = null,
+ content: @Composable () -> Unit = {}
+) {
+ Layout(
+ content = content,
+ modifier = Modifier
+ .offset { IntOffset(x, y) }
+ .focusRequester(focusRequester ?: FocusRequester())
+ .onFocusChanged { if (isFocused != null) isFocused.value = it.isFocused }
+ .focusModifier(),
+ measurePolicy = remember(width, height) {
+ MeasurePolicy { measurables, _ ->
+ val constraint = Constraints(width, width, height, height)
+ layout(width, height) {
+ measurables.forEach {
+ val placeable = it.measure(constraint)
+ placeable.place(0, 0)
+ }
+ }
+ }
+ }
+ )
+}
+
+private val MutableList<MutableState<Boolean>>.values get() = this.map { it.value }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
new file mode 100644
index 0000000..652e5ca
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalThreeItemsTest.kt
@@ -0,0 +1,5078 @@
+/*
+ * 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.focus
+
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalThreeItemsTest(private val focusDirection: FocusDirection) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private lateinit var focusManager: FocusManager
+ private val initialFocus: FocusRequester = FocusRequester()
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "direction={0}")
+ fun initParameters() = listOf(Left, Right, Up, Down)
+ }
+
+ /**
+ * __________ __________ * __________
+ * | Next | | Closer | * ^ | Next |
+ * | Item | | Item | * | | Item |
+ * |_________| |_________| * Direction |_________|
+ * ____________ * of Search
+ * | focused | * |
+ * | Item | * |
+ * |___________| * ____________
+ * * | focused | __________
+ * * | Item | | Closer |
+ * <---- Direction of Search --- * |___________| | Item |
+ * * |_________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ _________ * __________
+ * | Closer | | Next | * | Closer |
+ * | Item | | Item | * ____________ | Item |
+ * |_________| |________| * | focused | |_________|
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | *
+ * |___________| * | _________
+ * * Direction | Next |
+ * ---- Direction of Search ---> * of Search | Item |
+ * * | |________|
+ * * V
+ */
+ @MediumTest
+ @Test
+ fun validItemIsPickedEvenThoughThereIsACloserItem1() {
+ // Arrange.
+ val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 0, 20, 20)
+ FocusableBox(nextItem, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 40, 20, 20)
+ FocusableBox(nextItem, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(nextItem, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(nextItem, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(nextItem.value).isTrue()
+ }
+ }
+
+ /**
+ * * __________
+ * * | Next | ^
+ * ____________ * | Item | |
+ * | focused | * |_________| Direction
+ * | Item | * of Search
+ * |___________| * |
+ * _________ __________ * |
+ * | Next | | Closer | * ____________
+ * | Item | | Item | * __________ | focused |
+ * |________| |_________| * | Closer | | Item |
+ * <---- Direction of Search --- * | Item | |___________|
+ * * |_________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * __________
+ * | focused | * | Closer |
+ * | Item | * | Item | ____________
+ * |___________| * |_________| | focused |
+ * __________ _________ * | Item |
+ * | Closer | | Next | * |___________|
+ * | Item | | Item | *
+ * |_________| |________| * _________ |
+ * * | Next | Direction
+ * ---- Direction of Search ---> * | Item | of Search
+ * * |________| |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun validItemIsPickedEvenThoughThereIsACloserItem2() {
+ // Arrange.
+ val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 30, 20, 20)
+ FocusableBox(nextItem, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 40, 20, 20)
+ FocusableBox(nextItem, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(nextItem, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(nextItem, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(nextItem.value).isTrue()
+ }
+ }
+
+ /**
+ * _________ * _________
+ * | Next | * | Next | ^
+ * | Item | * | Item | |
+ * |________| * |________| Direction
+ * ____________ * of Search
+ * | focused | * |
+ * | Item | * |
+ * |___________| * ____________
+ * __________ * | focused |
+ * | Closer | * | Item | __________
+ * | Item | * |___________| | Closer |
+ * |_________| * | Item |
+ * <---- Direction of Search --- * |_________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * __________
+ * | Closer | * | Closer |
+ * | Item | * ____________ | Item |
+ * |_________| * | focused | |_________|
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * |
+ * |___________| * _________ Direction
+ * _________ * | Next | of Search
+ * | Next | * | Item | |
+ * | Item | * |________| |
+ * |________| * V
+ * ---- Direction of Search ---> *
+ * *
+ * *
+ */
+ @LargeTest
+ @Test
+ fun validItemIsPickedEvenThoughThereIsACloserItem3() {
+ // Arrange.
+ val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 60, 20, 20)
+ FocusableBox(nextItem, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 40, 20, 20)
+ FocusableBox(nextItem, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(nextItem, 40, 60, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 10, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 0, 20, 20)
+ FocusableBox(nextItem, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(nextItem.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _________
+ * | Closer | * ^ | Next |
+ * | Item | * | | Item |
+ * |_________| * Direction |________|
+ * ____________ * of Search
+ * | focused | * |
+ * | Item | * |
+ * |___________| * ____________
+ * _________ * | focused |
+ * | Next | * __________ | Item |
+ * | Item | * | Closer | |___________|
+ * |________| * | Item |
+ * <---- Direction of Search --- * |_________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _________ * __________
+ * | Next | * | Closer |
+ * | Item | * | Item | ____________
+ * |________| * |_________| | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * |
+ * |___________| * Direction _________
+ * __________ * of Search | Next |
+ * | Closer | * | | Item |
+ * | Item | * | |________|
+ * |_________| * V
+ * ---- Direction of Search ---> *
+ * *
+ * *
+ */
+ @LargeTest
+ @Test
+ fun validItemIsPickedEvenThoughThereIsACloserItem4() {
+ // Arrange.
+ val (focusedItem, closerItem, nextItem) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 0, 20, 20)
+ FocusableBox(nextItem, 0, 60, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 40, 20, 20)
+ FocusableBox(nextItem, 60, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 60, 20, 20)
+ FocusableBox(nextItem, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(nextItem, 60, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(nextItem.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ____________ ^
+ * | Closer | * | Item | |
+ * | Item | * | in beam | Direction
+ * |_________| * |___________| of Search
+ * __________ ____________ * |
+ * | Item | | focused | * |
+ * | in beam | | Item | * __________
+ * |_________| |___________| * ____________ | Closer |
+ * * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |___________| | Closer |
+ * ____________ __________ * | Item |
+ * | focused | | Item | * |_________|
+ * | Item | | in beam | *
+ * |___________| |_________| * ____________
+ * * | Item | |
+ * ---- Direction of Search ---> * | in beam | Direction
+ * * |___________| of Search
+ * * |
+ * * V
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ___________ * _________ ^
+ * | Closer | * | Item | |
+ * | Item | * | in beam| Direction
+ * |__________| * |________| of Search
+ * _______________ * |
+ * | Item in Beam | ____________ * |
+ * |______________| | focused | * __________
+ * | Item | * ____________ | Closer |
+ * |___________| * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |___________| | Closer |
+ * _______________ * | Item |
+ * ____________ | Item in Beam | * |_________|
+ * | focused | |______________| *
+ * | Item | * _________
+ * |___________| * | Item | |
+ * * | in beam| Direction
+ * ---- Direction of Search ---> * |________| of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 40, 20, 20)
+ FocusableBox(itemInBeam, 60, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 10, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 60, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(itemInBeam, 60, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ___________ * _________ ^
+ * | Closer | * | Item | |
+ * | Item | * | in beam| Direction
+ * |__________| * |________| of Search
+ * _______________ ____________ * |
+ * | Item in Beam | | focused | * |
+ * |______________| | Item | * __________
+ * |___________| * ____________ | Closer |
+ * * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |___________| | Closer |
+ * ____________ _______________ * | Item |
+ * | focused | | Item in Beam | * |_________|
+ * | Item | |______________| *
+ * |___________| * _________
+ * * | Item | |
+ * ---- Direction of Search ---> * | in beam| Direction
+ * * |________| of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis3() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _______ ^
+ * | Closer | * | Item | |
+ * | Item | * | in | Direction
+ * |_________| * | Beam | of Search
+ * ____________ * |______| |
+ * _______________ | | * |
+ * | Item in Beam | | focused | * __________
+ * |______________| | Item | * ____________ | Closer |
+ * |___________| * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |___________| | Closer |
+ * ____________ * | Item |
+ * | | _______________ * |_________|
+ * | focused | | Item in Beam | *
+ * | Item | |______________| * _______
+ * |___________| * | Item | |
+ * * | in | Direction
+ * * | Beam | of Search
+ * ---- Direction of Search ---> * |______| |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis4() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 30, 20, initialFocus)
+ FocusableBox(closerItem, 40, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 40, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _________ ^
+ * | Closer | * | | |
+ * | Item | * | Item | Direction
+ * |_________| * | in beam| of Search
+ * ____________ * |________| |
+ * _______________ | focused | * |
+ * | Item in Beam | | Item | * __________
+ * |______________| |___________| * ______________ | Closer |
+ * * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |_____________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * _____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |____________| | Closer |
+ * ____________ * | Item |
+ * | focused | _______________ * |_________|
+ * | Item | | Item in Beam | *
+ * |___________| |______________| * _________
+ * * | | |
+ * ---- Direction of Search ---> * | Item | Direction
+ * * | in beam| of Search
+ * * | | |
+ * * |________| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis5() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ^
+ * | Closer | * _________ |
+ * | Item | * | Item | Direction
+ * |_________| * | in beam| of Search
+ * ____________ * |________| |
+ * | focused | * |
+ * _______________ | Item | * __________
+ * | | |___________| * | Closer |
+ * | Item in Beam | * ____________ | Item |
+ * |______________| * | focused | |_________|
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * |___________| | Closer |
+ * ____________ * | Item |
+ * | focused | * |_________|
+ * | Item | _______________ *
+ * |___________| | | * _________
+ * | Item in Beam | * | Item | |
+ * |______________| * | in beam | Direction
+ * * |________ | of Search
+ * ---- Direction of Search ---> * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis6() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 10, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 40, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ____________ ^
+ * | Closer | * | Item | |
+ * | Item | * | in beam | Direction
+ * __________ |_________|________ * |___________| of Search
+ * | Item | | focused | * |
+ * | in beam | | Item | * _________ |
+ * |_________| |___________| * ___________| Closer |
+ * * | focused | Item |
+ * * | Item |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item |__________
+ * _______|_________| __________ * |___________| Closer |
+ * | focused | | Item | * | Item |
+ * | Item | | in beam | * |_________|
+ * |___________| |_________| *
+ * * ____________
+ * ---- Direction of Search ---> * | Item | |
+ * * | in beam | Direction
+ * * |___________| of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis7() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 20, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * _________ ^
+ * ___________ * | Item | |
+ * | Closer | * | in beam| Direction
+ * _______________ | Item | * |________| of Search
+ * | Item in Beam | |__________|________ * |
+ * |______________| | focused | * |
+ * | Item | * __________
+ * |___________| * ___________| Closer |
+ * * | focused | Item |
+ * * | Item |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | _______________ * | Item |__________
+ * _______|_________| | Item in Beam | * |___________| Closer |
+ * | focused | |______________| * | Item |
+ * | Item | * |_________|
+ * |___________| *
+ * * _________
+ * * | Item | |
+ * ---- Direction of Search ---> * | in beam| Direction
+ * * |________| of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis8() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ___________ * _________ ^
+ * | Closer | * | | |
+ * | Item | * | Item | Direction
+ * _______________ |__________|________ * | in beam| of Search
+ * | Item in Beam | | focused | * |________| |
+ * |______________| | Item | * |
+ * |___________| * __________
+ * * ___________| Closer |
+ * * | focused | Item |
+ * <---- Direction of Search --- * | Item |_________|
+ * * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item |__________
+ * _______|_________| _______________ * |___________| Closer |
+ * | focused | | Item in Beam | * | Item |
+ * | Item | |______________| * |_________|
+ * |___________| *
+ * * _________
+ * * | | |
+ * ---- Direction of Search ---> * | Item | Direction
+ * * | in beam| of Search
+ * * |________| |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis9() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 20, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _______ ^
+ * | Closer | * | Item | |
+ * | Item | * | in | Direction
+ * |_________|__________ * | Beam | of Search
+ * _______________ | | * |______| |
+ * | Item in Beam | | focused | * |
+ * |______________| | Item | * __________
+ * |___________| * ____________| Closer |
+ * * | focused | Item |
+ * <---- Direction of Search --- * | Item |_________|
+ * * |____________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * _____________
+ * | Closer | * | focused |
+ * | Item | * | Item |__________
+ * _______|_________| * |____________| Closer |
+ * | | _______________ * | Item |
+ * | focused | | Item in Beam | * |_________|
+ * | Item | |______________| *
+ * |___________| * _______
+ * * | Item | |
+ * * | in | Direction
+ * ---- Direction of Search ---> * | Beam | of Search
+ * * |______| |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis10() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 30, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _________ ^
+ * | Closer | * | | |
+ * | Item | * | Item | Direction
+ * |_________|______ * | in beam| of Search
+ * _______________ | focused | * |________| |
+ * | Item in Beam | | Item | * |
+ * |______________| |___________| * __________
+ * * ____________| Closer |
+ * * | focused | Item |
+ * * | Item |_________|
+ * <---- Direction of Search --- * |____________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * _____________
+ * __________ * | focused |
+ * | Closer | * | Item |__________
+ * | Item | * |____________| Closer |
+ * _______|_________| * | Item |
+ * | focused | _______________ * |_________|
+ * | Item | | Item in Beam | *
+ * |___________| |______________| * _________
+ * * | | |
+ * ---- Direction of Search ---> * | Item | Direction
+ * * | in beam| of Search
+ * * |________| |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis11() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * _________ ^
+ * | Closer | * | Item | |
+ * | Item | * | in beam| Direction
+ * |_________|_________ * |________| of Search
+ * | focused | * |
+ * _______________ | Item | * |
+ * | Item in Beam | |___________| * __________
+ * |______________| * ___________| Closer |
+ * * | focused | Item |
+ * <---- Direction of Search --- * | Item |_________|
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item |__________
+ * _______|_________| * |___________| Closer |
+ * | focused | * | Item |
+ * | Item | _______________ * |_________|
+ * |___________| | Item in Beam | *
+ * |______________| * _________
+ * * | Item | |
+ * ---- Direction of Search ---> * | in beam| Direction
+ * * |________| of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis12() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 10, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ ____________ * ^ ____________
+ * | Item | | focused | * | | Item |
+ * | in beam | | Item | * Direction | in beam |
+ * |_________| |___________| * of Search |___________|
+ * __________ * |
+ * | Closer | * |
+ * | Item | * __________
+ * |_________| * | Closer | ____________
+ * * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ____________ __________ * | focused |
+ * | focused | | Item | * __________ | Item |
+ * | Item | | in beam | * | Closer | |___________|
+ * |___________| |_________| * | Item |
+ * __________ * |_________|
+ * | Closer | *
+ * | Item | * ____________
+ * |_________| * | | Item |
+ * * Direction | in beam |
+ * ---- Direction of Search ---> * of Search |___________|
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis13() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * _______________ * ^ _________
+ * | Item in Beam | ____________ * | | Item |
+ * |______________| | focused | * Direction | in beam|
+ * | Item | * of Search |________|
+ * |___________| * |
+ * ___________ * |
+ * | Closer | * __________
+ * | Item | * | Closer | ____________
+ * |__________| * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ * ____________
+ * ____________ | Item in Beam | * | focused |
+ * | focused | |______________| * __________ | Item |
+ * | Item | * | Closer | |___________|
+ * |___________| * | Item |
+ * __________ * |_________|
+ * | Closer | *
+ * | Item | * _________
+ * |_________| * | | Item |
+ * * Direction | in beam|
+ * ---- Direction of Search ---> * of Search |________|
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis14() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 40, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 40, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^ _________
+ * _______________ ____________ * | | Item |
+ * | Item in Beam | | focused | * Direction | in beam|
+ * |______________| | Item | * of Search |________|
+ * |___________| * |
+ * ___________ * |
+ * | Closer | * __________
+ * | Item | * | Closer | ____________
+ * |__________| * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ____________ _______________ * | focused |
+ * | focused | | Item in Beam | * __________ | Item |
+ * | Item | |______________| * | Closer | |___________|
+ * |___________| * | Item |
+ * __________ * |_________|
+ * | Closer | *
+ * | Item | * _________
+ * |_________| * | | Item |
+ * * Direction | in beam|
+ * ---- Direction of Search ---> * of Search |________|
+ * * |
+ * * V
+ *
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis15() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ____________ * ^ _______
+ * _______________ | | * | | Item |
+ * | Item in Beam | | focused | * Direction | in |
+ * |______________| | Item | * of Search | Beam |
+ * |___________| * | |______|
+ * __________ * |
+ * | Closer | * __________
+ * | Item | * | Closer | ____________
+ * |_________| * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ____________
+ * | | _______________ * | focused |
+ * | focused | | Item in Beam | * __________ | Item |
+ * | Item | |______________| * | Closer | |___________|
+ * |___________| * | Item |
+ * __________ * |_________|
+ * | Closer | *
+ * | Item | * _______
+ * |_________| * | | Item |
+ * * Direction | in |
+ * ---- Direction of Search ---> * of Search | Beam |
+ * * | |______|
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis16() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 40, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 30, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 10, 40, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 40, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^ _________
+ * ____________ * | | Item |
+ * _______________ | focused | * Direction | in beam|
+ * | Item in Beam | | Item | * of Search |________|
+ * |______________| |___________| * |
+ * __________ * |
+ * | Closer | * __________
+ * | Item | * | Closer | _____________
+ * |_________| * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |____________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * _____________
+ * | focused | _______________ * | focused |
+ * | Item | | Item in Beam | * __________ | Item |
+ * |___________| |______________| * | Closer | |____________|
+ * __________ * | Item |
+ * | Closer | * |_________|
+ * | Item | *
+ * |_________| * _________
+ * * | | Item |
+ * ---- Direction of Search ---> * Direction | in beam|
+ * * of Search |________|
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis17() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 30, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ____________ * ^ _________
+ * | focused | * | | Item |
+ * _______________ | Item | * Direction | in beam|
+ * | | |___________| * of Search |________|
+ * | Item in Beam | * |
+ * |______________| * |
+ * __________ * __________
+ * | Closer | * | Closer | ____________
+ * | Item | * | Item | | focused |
+ * |_________| * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ____________
+ * | focused | * | focused |
+ * | Item | _______________ * __________ | Item |
+ * |___________| | | * | Closer | |___________|
+ * | Item in Beam | * | Item |
+ * |______________| * |_________|
+ * *
+ * __________ * | __________
+ * | Closer | * Direction | Item |
+ * | Item | * of Search | in beam |
+ * |_________| * | |_________|
+ * * V
+ * ---- Direction of Search ---> *
+ * *
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis18() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 40, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 40, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 40, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * __________ ____________ * ____________ |
+ * | Item | | focused | * | Item | Direction
+ * | in beam | | Item | * | in beam | of Search
+ * |_________| _____|___________| * |___________| |
+ * | Closer | * __________ |
+ * | Item | * | Closer |
+ * |_________| * ___________| Item |
+ * * | focused |_________|
+ * <---- Direction of Search --- * | Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * * | focused |
+ * * ________| Item |
+ * ____________ __________ * | Closer |___________|
+ * | focused | | Item | * | Item |
+ * | Item | | in beam | * |________|
+ * |___________|______ |_________| * ____________
+ * | Closer | * | | Item |
+ * | Item | * Direction | in beam |
+ * |_________| * of Search |___________|
+ * * |
+ * ---- Direction of Search ---> * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis19() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 20, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^ _________
+ * _______________ * | | Item |
+ * | Item in Beam | ____________ * Direction | in beam|
+ * |______________| | focused | * of Search |________|
+ * | Item | * |
+ * ______|___________| * |
+ * | Closer | * __________
+ * | Item | * | Closer |___________
+ * |__________| * | Item | focused |
+ * * |_________| Item |
+ * <---- Direction of Search --- * |__________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ * ____________
+ * ____________ | Item in Beam | * | focused |
+ * | focused | |______________| * _________| Item |
+ * | Item | * | Closer |___________|
+ * |___________|______ * | Item |
+ * | Closer | * |_________|
+ * | Item | *
+ * |_________| * | _________
+ * * Direction | Item |
+ * ---- Direction of Search ---> * of Search | in beam|
+ * * | |________|
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis20() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 10, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^ _________
+ * _______________ ____________ * | | Item |
+ * | Item in Beam | | focused | * Direction | in beam|
+ * |______________| | Item | * of Search |________|
+ * ______|___________| * |
+ * | Closer | * __________
+ * | Item | * | Closer |
+ * |__________| * | Item |_______________
+ * * |_________| focused |
+ * <---- Direction of Search --- * | Item |
+ * * |______________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * _______________
+ * * | focused |
+ * * _________| Item |
+ * ____________ _______________ * | Closer |______________|
+ * | focused | | Item in Beam | * | Item |
+ * | Item | |______________| * |_________|
+ * |___________|______ *
+ * | Closer | * | _________
+ * | Item | * Direction | Item |
+ * |_________| * of Search | in beam|
+ * * | |________|
+ * * V
+ * ---- Direction of Search ---> *
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis21() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ____________ * _______ ^
+ * _______________ | | * | Item | |
+ * | Item in Beam | | focused | * | in | Direction
+ * |______________| | Item | * | Beam | of Search
+ * _______|___________| * |______| |
+ * | Closer | * |
+ * | Item | * __________
+ * |_________| * | Closer |
+ * * | Item |______________
+ * <---- Direction of Search --- * |_________| focused |
+ * * | Item |
+ * * |_____________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ______________
+ * | | _______________ * | focused |
+ * | focused | | Item in Beam | * _________| Item |
+ * | Item | |______________| * | Closer |_____________|
+ * |___________|______ * | Item |
+ * | Closer | * |_________|
+ * | Item | *
+ * |_________| * _______ |
+ * * | Item | Direction
+ * ---- Direction of Search ---> * | in | of Search
+ * * | Beam | |
+ * * |______| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis22() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 40, 30, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * _________ ^
+ * ____________ * | Item | |
+ * _______________ | focused | * | in beam| Direction
+ * | Item in Beam | | Item | * |________| of Search
+ * |______________| ___|___________| * |
+ * | Closer | * __________ |
+ * | Item | * | Closer |
+ * |_________| * | Item |____________
+ * * |_________| focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ____________
+ * | focused | _______________ * | focused |
+ * | Item | | Item in Beam | * _________| Item |
+ * |___________|______ |______________| * | Closer |___________|
+ * | Closer | * | Item | |
+ * | Item | * |_________| Direction
+ * |_________| * _________ of Search
+ * * | Item | |
+ * * | in beam| V
+ * ---- Direction of Search ---> * |________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis23() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 20, 0, 10, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 20, 40, 10, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * _________ ^
+ * ____________ * | Item | |
+ * | focused | * | in beam| Direction
+ * _______________ | Item | * |________| of Search
+ * | Item in Beam | __|___________| * |
+ * |______________| | Closer | * |
+ * | Item | * __________
+ * |_________| * | Closer |____________
+ * * | Item | focused |
+ * <---- Direction of Search --- * |_________| Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ____________
+ * | focused | * | focused |
+ * | Item | _______________ * _________| Item |
+ * |___________|______ | Item in Beam | * | Closer |___________|
+ * | Closer | |______________| * | Item |
+ * | Item | * |_________|
+ * |_________| * |
+ * * _________ Direction
+ * ---- Direction of Search ---> * | Item | of Search
+ * * | in beam| |
+ * * |________| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemWithOverlappingMajorAxis24() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 20, 20)
+ FocusableBox(itemInBeam, 40, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 10, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ^
+ * | Closer | * |
+ * | Item | * ____________ Direction
+ * |_________| * | Item | of Search
+ * ______________________________ * | in beam | |
+ * | Item | | Focused | * |___________| |
+ * | in beam | | Item | * | | __________
+ * |__________|___|______________| * |___________| | Closer |
+ * * | focused | | Item |
+ * * | Item | |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ *
+ * | Closer | * ____________
+ * | Item | * | focused | __________
+ * |_________| * | Item | | Closer |
+ * ______________________________ * |___________| | Item |
+ * | focused | | Item | * | | |_________|
+ * | Item | | in beam | * |___________|
+ * |___________|____|____________| * | Item | |
+ * * | in beam | Direction
+ * ---- Direction of Search ---> * |___________| of Search
+ * * |
+ * * V
+ */
+ @MediumTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 40, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 40, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 20, 40, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 40)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 40, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 20, 30, 40, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 20, 40)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * * |
+ * __________ * Direction
+ * | Closer | * of Search
+ * | Item | * |
+ * |_________| * _________ |
+ * ________________________________ * | Item | __________
+ * | Item in Beam | | focused | * | in beam| | |
+ * |________________|__| Item | * __|________| | Closer |
+ * |______________| * | |________| | Item |
+ * * | focused | |_________|
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * | ________| | Closer |
+ * ______________________________ * |__|________| | Item |
+ * | focused | | Item in Beam | * | Item | |_________|
+ * | Item |__|______________| * | in beam| |
+ * |______________| * |________| Direction
+ * * of Search
+ * ---- Direction of Search ---> * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 30, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 30, 30, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 30, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * __________ * |
+ * | Closer | * Direction
+ * | Item | * of Search
+ * |_________| * |
+ * _______________ * _________ |
+ * ________________|___ focused | * | Item | __________
+ * | Item in Beam | | Item | * | in beam| | |
+ * |________________|__|___________| * |________|___ | Closer |
+ * * |________| | | Item |
+ * * | focused | |_________|
+ * <---- Direction of Search --- * | Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ *
+ * | Closer | * ____________
+ * | Item | * | focused |
+ * |_________| * | Item | __________
+ * _______________ * |_________ | | Closer |
+ * | focused __|_______________ * |________|__| | Item |
+ * | Item | | Item in Beam | * | Item | |_________|
+ * |___________|__|______________| * | in beam| |
+ * * |________| Direction
+ * ---- Direction of Search ---> * of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis3() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 30, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 40, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 0, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 40, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 0, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ^
+ * | Closer | * |
+ * | Item | * Direction
+ * |_________| * of Search
+ * _______________ * _________ |
+ * _______________|___ | * | Item | |
+ * | Item in Beam | | focused | * | in beam| __________
+ * |_______________|__| Item | * _|________|__ | Closer |
+ * |______________| * | |________| | | Item |
+ * * | focused | |_________|
+ * <---- Direction of Search --- * | Item |
+ * * |____________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item | __________
+ * |_________| * | _________ | | Closer |
+ * _______________ * |_|________|_| | Item |
+ * | __|________________ * | Item | |_________|
+ * | focused | | Item in Beam | * | in beam| |
+ * | Item |__|_______________| * |________| Direction
+ * |______________| * of Search
+ * * |
+ * * V
+ * ---- Direction of Search ---> *
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis4() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 30, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 40, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 40, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 40, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 40, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ *
+ * | Closer | * ____________ ^
+ * | Item | * | Item | |
+ * ____________|_________|_______ * | in beam | Direction
+ * | Item | | Focused | * |___________| of Search
+ * | in beam | | Item | * | |__________ |
+ * |__________|___|______________| * |___________| Closer | |
+ * * | focused | Item |
+ * * | Item |_________|
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |__________
+ * | Item | * | Item | Closer |
+ * ____|_________|______________ * |___________| Item | |
+ * | focused | | Item | * | |_________| Direction
+ * | Item | | in beam | * |___________| of Search
+ * |___________|___|____________| * | Item | |
+ * * | in beam | V
+ * ---- Direction of Search ---> * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis5() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 10, 20, 40, 20, initialFocus)
+ FocusableBox(closerItem, 20, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 30, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 10, 20, 40, initialFocus)
+ FocusableBox(closerItem, 20, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 40, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 20, 20, 30, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 20, 20, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * __________ * |
+ * | Closer | * Direction
+ * | Item | * _________ of Search
+ * ____________|_________|_________ * | Item |__________ |
+ * | Item in Beam | | focused | * | in beam| | |
+ * |________________|__| Item | * __|________| Closer |
+ * |______________| * | |________| Item |
+ * * | focused |_________|
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * __________ * | focused |
+ * | Closer | * | Item |__________
+ * | Item | * | ________| |
+ * _______|_________|____________ * |__|________| Closer | |
+ * | focused | | Item in Beam | * | Item | Item | Direction
+ * | Item |__|______________| * | in beam|_________| of Search
+ * |______________| * |________| |
+ * * V
+ * ---- Direction of Search ---> *
+ * *
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis6() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 20, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 20, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 20, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ *
+ * | Closer | *
+ * | Item | * ^
+ * |_________|_________ * _________ |
+ * ________________|___ focused | * | Item | __________ Direction
+ * | Item in Beam | | Item | * | in beam| | | of Search
+ * |________________|__|___________| * |________|__| Closer | |
+ * * |________| | Item | |
+ * * | focused |_________|
+ * <---- Direction of Search --- * | Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * ____________
+ * | Closer | * | focused |
+ * | Item | * | Item |
+ * _______|_________| * | |__________
+ * | focused __|_______________ * |_________ | Closer | |
+ * | Item | | Item in Beam | * |________|__| Item | Direction
+ * |___________|__|______________| * | Item | |_________| of Search
+ * * | in beam| |
+ * ---- Direction of Search ---> * |________| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis7() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 20, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 30, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 30)
+ FocusableBox(itemInBeam, 0, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 30, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 20, 10, 20, 30)
+ FocusableBox(itemInBeam, 0, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ^
+ * | Closer | * |
+ * | Item | * Direction
+ * |_________|_________ * _________ of Search
+ * _______________|___ | * | Item | |
+ * | Item in Beam | | focused | * | in beam| __________ |
+ * |_______________|__| Item | * _|________|_| Closer |
+ * |______________| * | |________| | Item |
+ * * | focused |_________|
+ * <---- Direction of Search --- * | Item |
+ * * |____________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * _____________
+ * | Closer | * | focused |
+ * | Item | * | Item |__________
+ * _______|_________| * | _________ | Closer |
+ * | __|________________ * |_|________|_| Item |
+ * | focused | | Item in Beam | * | Item | |_________| |
+ * | Item |__|_______________| * | in beam| Direction
+ * |______________| * |________| of Search
+ * * |
+ * * V
+ * ---- Direction of Search ---> *
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis8() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 30, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 0, 30, 20)
+ FocusableBox(itemInBeam, 20, 30, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 30)
+ FocusableBox(itemInBeam, 10, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * ______________________________ * |
+ * | Item | | Focused | * Direction ____________
+ * | in beam | | Item | * of Search | Item |
+ * |__________|___|______________| * | | in beam |
+ * __________ * | |___________|
+ * | Closer | * __________ | |
+ * | Item | * | Closer | |___________|
+ * |_________| * | Item | | focused |
+ * * |_________| | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _____________________________ *
+ * | focused | | Item | * ____________
+ * | Item | | in beam | * __________ | focused |
+ * |___________|___|____________| * | Closer | | Item |
+ * __________ * | Item | |___________|
+ * | Closer | * |_________| | |
+ * | Item | * |___________|
+ * |_________| * | | Item |
+ * * Direction | in beam |
+ * ---- Direction of Search ---> * of Search |___________|
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis9() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 30, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 10, 20, 40, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 20, 0, 30, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 40, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 20, 20, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * * |
+ * ________________________________ * Direction
+ * | Item in Beam | | focused | * of Search
+ * |________________|__| Item | * |
+ * |______________| * | _________
+ * __________ * __________ | Item |
+ * | Closer | * | | | in beam|
+ * | Item | * | Closer | __|________|
+ * |_________| * | Item | | |________|
+ * * |_________| | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ______________________________ * ____________
+ * | focused | | Item in Beam | * __________ | focused |
+ * | Item |__|______________| * | Closer | | Item |
+ * |______________| * | Item | | ________|
+ * __________ * | | |__|________|
+ * | Closer | * |_________| | Item |
+ * | Item | * | | in beam|
+ * |_________| * Direction |________|
+ * * of Search
+ * ---- Direction of Search ---> * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis10() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 0, 0, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 40, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 20, 0, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 40, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * ^
+ * * |
+ * _______________ * Direction
+ * ________________|___ focused | * of Search
+ * | Item in Beam | | Item | * |
+ * |________________|__|___________| * | _________
+ * __________ * __________ | Item |
+ * | Closer | * | | | in beam|
+ * | Item | * | Closer | |________|___
+ * |_________| * | Item | |________| |
+ * * |_________| | focused |
+ * <---- Direction of Search --- * | Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ *
+ * | focused __|_______________ * ____________
+ * | Item | | Item in Beam | * | focused |
+ * |___________|__|______________| * __________ | Item |
+ * __________ * | Closer | |_________ |
+ * | Closer | * | Item | |________|__|
+ * | Item | * |_________| | Item |
+ * |_________| * | | in beam|
+ * * Direction |________|
+ * ---- Direction of Search ---> * of Search
+ * * |
+ * * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis11() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 0, 10, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 30, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 20, 10, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 30, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * _______________ * ^
+ * _______________|___ | * |
+ * | Item in Beam | | focused | * Direction
+ * |_______________|__| Item | * of Search
+ * |______________| * | _________
+ * __________ * | | Item |
+ * | Closer | * __________ | in beam|
+ * | Item | * | Closer | _|________|__
+ * |_________| * | Item | | |________| |
+ * * |_________| | focused |
+ * <---- Direction of Search --- * | Item |
+ * * |____________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ * ____________
+ * | __|________________ * | focused |
+ * | focused | | Item in Beam | * __________ | Item |
+ * | Item |__|_______________| * | Closer | | _________ |
+ * |______________| * | Item | |_|________|_|
+ * __________ * |_________| | Item |
+ * | Closer | * | | in beam|
+ * | Item | * Direction |________|
+ * |_________| * of Search
+ * * |
+ * ---- Direction of Search ---> * V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis12() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 40, 30, 20)
+ FocusableBox(itemInBeam, 0, 10, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 40, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 40, 30, 20)
+ FocusableBox(itemInBeam, 20, 10, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 40, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ______________________________ * ____________ ^
+ * | Item | | Focused | * | Item | |
+ * | in beam | | Item | * | in beam | Direction
+ * |__________|___|______________| * |___________| of Search
+ * | Closer | * _________| | |
+ * | Item | * | Closer |___________| |
+ * |_________| * | Item | focused |
+ * * |_________| Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _____________________________ * ____________
+ * | focused | | Item | * _________| focused |
+ * | Item | | in beam | * | Closer | Item |
+ * |___________|___|____________| * | Item |___________|
+ * | Closer | * |_________| | |
+ * | Item | * |___________| Direction
+ * |_________| * | Item | of Search
+ * * | in beam | |
+ * ---- Direction of Search ---> * |___________| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis13() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+ FocusableBox(closerItem, 20, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 30, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 10, 20, 40, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 20, 0, 20, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 20, 20)
+ FocusableBox(itemInBeam, 20, 0, 30, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 40, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 20, 20, 20, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ________________________________ * ^
+ * | Item in Beam | | focused | * _________ |
+ * |________________|__| Item | * __________ | Item | Direction
+ * ___|______________| * | | | in beam| of Search
+ * | Closer | * | Closer |__|________| |
+ * | Item | * | Item | |________| |
+ * |_________| * |_________| focused | |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ______________________________ * ____________
+ * | focused | | Item in Beam | * __________| focused |
+ * | Item |__|______________| * | Closer | |
+ * |______________|___ * | Item | ________| |
+ * | Closer | * | |__|________| Direction
+ * | Item | * |_________| | | of Search
+ * |_________| * | Item | |
+ * * | in beam| V
+ * ---- Direction of Search ---> * |________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis14() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 30, 20)
+ FocusableBox(itemInBeam, 0, 0, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 30, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 30, 20)
+ FocusableBox(itemInBeam, 20, 0, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 20, 30, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * _______________ *
+ * ________________|___ focused | * _________ ^
+ * | Item in Beam | | Item | * _________| Item | |
+ * |________________|__|___________| * | | in beam| Direction
+ * | Closer | * | Closer |________|__ of Search
+ * | Item | * | Item |________| | |
+ * |_________| * |_________| focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ * ____________
+ * | focused __|_______________ * _________| focused |
+ * | Item | | Item in Beam | * | | Item |
+ * |___________|__|______________| * | Closer |_________ |
+ * | Closer | * | Item |________|__| |
+ * | Item | * |_________| | Direction
+ * |_________| * | Item | of Search
+ * * | in beam| |
+ * ---- Direction of Search ---> * |________| V
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis15() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 30, 20)
+ FocusableBox(itemInBeam, 0, 10, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 20, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 20, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 20, initialFocus)
+ FocusableBox(closerItem, 10, 20, 30, 20)
+ FocusableBox(itemInBeam, 20, 10, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 20, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 20, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * _______________ * _________
+ * _______________|___ | * | Item |
+ * | Item in Beam | | focused | * __________ | in beam| ^
+ * |_______________|__| Item | * | Closer |_|________|__ |
+ * ___|______________| * | Item | |________| | Direction
+ * | Closer | * |_________| focused | of Search
+ * | Item | * | Item | |
+ * |_________| * |____________| |
+ * *
+ * <---- Direction of Search --- *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _______________ * _____________
+ * | __|________________ * | focused |
+ * | focused | | Item in Beam | * _________| Item | |
+ * | Item |__|_______________| * | Closer | _________ | Direction
+ * |______________|___ * | Item |_|________|_| of Search
+ * | Closer | * |_________| | Item | |
+ * | Item | * | in beam| V
+ * |_________| * |________|
+ * *
+ * ---- Direction of Search ---> *
+ */
+ @LargeTest
+ @Test
+ fun inBeamOverlappingItemWinsOverCloserItemWithOverlappingMajorAxis16() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 0, 10, 30, 10)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 20, 20, 30, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 30)
+ FocusableBox(itemInBeam, 30, 0, 10, 30)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 10, 30, 30, 20)
+ FocusableBox(itemInBeam, 20, 10, 30, 10)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus)
+ FocusableBox(closerItem, 0, 10, 0, 10)
+ FocusableBox(itemInBeam, 30, 20, 10, 30)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ * ____________ __________
+ * | Closer | * | Item | | Closer |
+ * | Item | * | in beam | | Item |
+ * |_________| * |___________| |_________|
+ * __________ ____________ *
+ * | Item | | focused | *
+ * | in beam | | Item | * ^
+ * |_________| |___________| * ____________ |
+ * * | focused | Direction
+ * * | Item | of Search
+ * <---- Direction of Search --- * |___________| |
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ *
+ * | Closer | *
+ * | Item | * ____________ |
+ * |_________| * | focused | Direction
+ * ____________ __________ * | Item | of Search
+ * | focused | | Item | * |___________| |
+ * | Item | | in beam | * V
+ * |___________| |_________| *
+ * * ____________ __________
+ * ---- Direction of Search ---> * | Item | | Closer |
+ * * | in beam | | Item |
+ * * |___________| |_________|
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverOtherItemWithSameMajorAxisDistance1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 30, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * * __________ ____________
+ * __________ ____________ * | Closer | | Item |
+ * | Item | | focused | * | Item | | in beam |
+ * | in beam | | Item | * |_________| |___________|
+ * |_________| |___________| *
+ * __________ *
+ * | Closer | * ^
+ * | Item | * | ____________
+ * |_________| * Direction | focused |
+ * * of Search | Item |
+ * <---- Direction of Search --- * | |___________|
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * | ____________
+ * | focused | | Item | * Direction | focused |
+ * | Item | | in beam | * of Search | Item |
+ * |___________| |_________| * | |___________|
+ * __________ * V
+ * | Closer | *
+ * | Item | * __________ ____________
+ * |_________| * | Closer | | Item |
+ * * | Item | | in beam |
+ * ---- Direction of Search ---> * |_________| |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverOtherItemWithSameMajorAxisDistance2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 30, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * _____________ * ____________ __________
+ * | Closer | * | Item | | |
+ * | Item | * | in beam | | Closer |
+ * |____________| * |___________| | Item |
+ * __________ ____________ * |_________|
+ * | Item | | focused | *
+ * | in beam | | Item | * ^
+ * |_________| |___________| * ____________ |
+ * * | focused | Direction
+ * * | Item | of Search
+ * <---- Direction of Search --- * |___________| |
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * _____________ *
+ * | Closer | *
+ * | Item | * ____________ |
+ * |____________| * | focused | Direction
+ * ____________ __________ * | Item | of Search
+ * | focused | | Item | * |___________| |
+ * | Item | | in beam | * V
+ * |___________| |_________| * __________
+ * * ____________ | |
+ * ---- Direction of Search ---> * | Item | | Closer |
+ * * | in beam | | Item |
+ * * |___________| |_________|
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverOtherItemWithSameFarEdge1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 30, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 30)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 30, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 30)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ ____________ * __________ ____________
+ * | Item | | focused | * | | | Item |
+ * | in beam | | Item | * | Closer | | in beam |
+ * |_________| |___________| * | Item | |___________|
+ * * |_________|
+ * _____________ *
+ * | Closer | * ^
+ * | Item | * | ____________
+ * |____________| * Direction | focused |
+ * * of Search | Item |
+ * <---- Direction of Search --- * | |___________|
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * | ____________
+ * | focused | | Item | * Direction | focused |
+ * | Item | | in beam | * of Search | Item |
+ * |___________| |_________| * | |___________|
+ * _____________ * V
+ * | Closer | * __________
+ * | Item | * | | ____________
+ * |____________| * | Closer | | Item |
+ * * | Item | | in beam |
+ * ---- Direction of Search ---> * |_________| |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverOtherItemWithSameFarEdge2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 30, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 0, 20, 30)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 30, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 30)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ___________ * ____________
+ * | Closer | * | Item | __________
+ * | Item | * | in beam | | Closer |
+ * |__________| * |___________| | Item |
+ * __________ ____________ * |_________|
+ * | Item | | focused | *
+ * | in beam | | Item | * ^
+ * |_________| |___________| * ____________ |
+ * * | focused | Direction
+ * * | Item | of Search
+ * <---- Direction of Search --- * |___________| |
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ___________ *
+ * | Closer | *
+ * | Item | * ____________ |
+ * |__________| * | focused | Direction
+ * ____________ __________ * | Item | of Search
+ * | focused | | Item | * |___________| |
+ * | Item | | in beam | * V
+ * |___________| |_________| * __________
+ * * ____________ | Closer |
+ * ---- Direction of Search ---> * | Item | | Item |
+ * * | in beam | |_________|
+ * * |___________|
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverOtherItemWithFarEdgeGreaterThanInBeamCloserEdge1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 10, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * __________ ____________ * ____________
+ * | Item | | focused | * __________ | Item |
+ * | in beam | | Item | * | Closer | | in beam |
+ * |_________| |___________| * | Item | |___________|
+ * ___________ * |_________|
+ * | Closer | *
+ * | Item | * ^
+ * |__________| * | ____________
+ * * Direction | focused |
+ * <---- Direction of Search --- * of Search | Item |
+ * * | |___________|
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * | ____________
+ * | focused | | Item | * Direction | focused |
+ * | Item | | in beam | * of Search | Item |
+ * |___________| |_________| * | |___________|
+ * ___________ * V
+ * | Closer | * __________
+ * | Item | * | Closer | ____________
+ * |__________| * | Item | | Item |
+ * * |_________| | in beam |
+ * ---- Direction of Search ---> * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverOtherItemWithFarEdgeGreaterThanInBeamCloserEdge2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 10, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 10, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(closerItem.value).isFalse()
+ assertThat(focusedItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ }
+
+ /**
+ * ___________ * ____________
+ * | Closer | * | Item |
+ * | Item | * | in beam |
+ * |__________| * |___________| __________
+ * __________ ____________ * | Closer |
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * |_________|
+ * |_________| |___________| * ^
+ * * ____________ |
+ * * | focused | Direction
+ * <---- Direction of Search --- * | Item | of Search
+ * * |___________| |
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ___________ * ____________ |
+ * | Closer | * | focused | Direction
+ * | Item | * | Item | of Search
+ * |__________| * |___________| |
+ * ____________ __________ * V
+ * | focused | | Item | * _________
+ * | Item | | in beam | * | Closer |
+ * |___________| |_________| * | Item |
+ * * ____________ |________|
+ * ---- Direction of Search ---> * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverOtherItemWithFarEdgeEqualToInBeamCloserEdge_forHorizontalSearch1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 50, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 50, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 50, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 50, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________ ____________ * ____________
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * | in beam |
+ * |_________| |___________| * __________ |___________|
+ * ___________ * | Closer |
+ * | Closer | * | Item |
+ * | Item | * |_________|
+ * |__________| * ^
+ * * | ____________
+ * <---- Direction of Search --- * Direction | focused |
+ * * of Search | Item |
+ * * | |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * | ____________
+ * | focused | | Item | * Direction | focused |
+ * | Item | | in beam | * of Search | Item |
+ * |___________| |_________| * | |___________|
+ * ___________ * V
+ * | Closer | * _________
+ * | Item | * | Closer |
+ * |__________| * | Item |
+ * * |________| ____________
+ * ---- Direction of Search ---> * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverOtherItemWithFarEdgeEqualToInBeamCloserEdge_forHorizontalSearch2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 50, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 50, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 50, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 50, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________ * ____________
+ * | Closer | * | Item |
+ * | Item | * | in beam |
+ * |__________| * |___________| _________
+ * __________ ____________ * | Closer |
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * ____________ |________|
+ * |_________| |___________| * | focused | ^
+ * * | Item | |
+ * * |___________| Direction
+ * <---- Direction of Search --- * of Search
+ * * |
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ___________ * |
+ * | Closer | * Direction
+ * | Item | * ____________ of Search
+ * |__________| * | focused | |
+ * ____________ __________ * | Item | V
+ * | focused | | Item | * |___________| _________
+ * | Item | | in beam | * | Closer |
+ * |___________| |_________| * | Item |
+ * * ____________ |________|
+ * ---- Direction of Search ---> * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @MediumTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch1() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 0, 20, 20)
+ FocusableBox(itemInBeam, 40, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________ * ____________
+ * | Closer | * | Item |
+ * | Item | * | in beam |
+ * |__________| * |___________|
+ * __________ ____________ * _________
+ * | Item | | focused | * | Closer |
+ * | in beam | | Item | * | Item |
+ * |_________| |___________| * |________|
+ * * ____________ ^
+ * * | focused | |
+ * <---- Direction of Search --- * | Item | Direction
+ * * |___________| of Search
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ *
+ * | Closer | * |
+ * | Item | * ____________ Direction
+ * |_________| * | focused | of Search
+ * ____________ __________ * | Item | |
+ * | focused | | Item | * |___________| V
+ * | Item | | in beam | * _________
+ * |___________| |_________| * | Closer |
+ * * | Item |
+ * ---- Direction of Search ---> * |________|
+ * * ____________
+ * * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch2() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 60, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________ * ____________
+ * | Closer | * | Item |
+ * | Item | * | in beam |
+ * |__________| * |___________|
+ * __________ ____________ * _________
+ * | Item | | focused | * | Closer |
+ * | in beam | | Item | * | Item |
+ * |_________| |___________| * ____________ |________|
+ * * | focused | ^
+ * * | Item | |
+ * <---- Direction of Search --- * |___________| Direction
+ * * of Search
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * __________ * |
+ * | Closer | * Direction
+ * | Item | * ____________ of Search
+ * |_________| * | focused | |
+ * ____________ __________ * | Item | V
+ * | focused | | Item | * |___________| _________
+ * | Item | | in beam | * | Closer |
+ * |___________| |_________| * | Item |
+ * * |________|
+ * ---- Direction of Search ---> * ____________
+ * * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch3() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 50, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 0, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 50, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 30, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 0, 20, 20)
+ FocusableBox(itemInBeam, 50, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 50, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________ ____________ * ____________
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * | in beam |
+ * |_________| |___________| * _________ |___________|
+ * ___________ * | Closer |
+ * | Closer | * | Item |
+ * | Item | * |________| ____________
+ * |__________| * ^ | focused |
+ * * | | Item |
+ * <---- Direction of Search --- * Direction |___________|
+ * * of Search
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * |
+ * | focused | | Item | * Direction
+ * | Item | | in beam | * of Search ____________
+ * |___________| |_________| * | | focused |
+ * ___________ * V | Item |
+ * | Closer | * _________ |___________|
+ * | Item | * | Closer |
+ * |__________| * | Item |
+ * * |________| ____________
+ * ---- Direction of Search ---> * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch4() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 40, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 0, 30, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 40, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 30, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________ ____________ * ____________
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * | in beam |
+ * |_________| |___________| * |___________|
+ * ___________ * _________
+ * | Closer | * | Closer |
+ * | Item | * | Item |
+ * |__________| * |________|
+ * * ^ ____________
+ * <---- Direction of Search --- * | | focused |
+ * * Direction | Item |
+ * * of Search |___________|
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ *
+ * | focused | | Item | * |
+ * | Item | | in beam | * Direction ____________
+ * |___________| |_________| * of Search | focused |
+ * __________ * | | Item |
+ * | Closer | * V |___________|
+ * | Item | * _________
+ * |_________| * | Closer |
+ * * | Item |
+ * ---- Direction of Search ---> * |________|
+ * * ____________
+ * * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch5() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 60, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________ ____________ * ____________
+ * | Item | | focused | * | Item |
+ * | in beam | | Item | * | in beam |
+ * |_________| |___________| * |___________|
+ * ___________ * _________
+ * | Closer | * | Closer |
+ * | Item | * | Item |
+ * |__________| * |________| ____________
+ * * ^ | focused |
+ * <---- Direction of Search --- * | | Item |
+ * * Direction |___________|
+ * * of Search
+ * * |
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ __________ * |
+ * | focused | | Item | * Direction
+ * | Item | | in beam | * of Search ____________
+ * |___________| |_________| * | | focused |
+ * __________ * V | Item |
+ * | Closer | * _________ |___________|
+ * | Item | * | Closer |
+ * |_________| * | Item |
+ * * |________|
+ * * ____________
+ * ---- Direction of Search ---> * | Item |
+ * * | in beam |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun inBeamWinsOverCloserItemForHorizontalSearchButNotForVerticalSearch6() {
+ // Arrange.
+ val (focusedItem, closerItem, itemInBeam) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 50, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 30, 30, 20, 20)
+ FocusableBox(itemInBeam, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 50, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 30, 20, 20)
+ FocusableBox(itemInBeam, 30, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 20, 30, 20, 20)
+ FocusableBox(itemInBeam, 50, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ FocusableBox(closerItem, 0, 20, 20, 20)
+ FocusableBox(itemInBeam, 30, 50, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ when (focusDirection) {
+ Left, Right -> {
+ assertThat(closerItem.value).isFalse()
+ assertThat(itemInBeam.value).isTrue()
+ }
+ Up, Down -> {
+ assertThat(closerItem.value).isTrue()
+ assertThat(itemInBeam.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________ ____________ ____________ * ____________
+ * | In Beam | | In Beam | | focused | * | In Beam |
+ * | Farther | | Closer | | Item | * | Farther |
+ * |___________| |___________| |___________| * |___________| ^
+ * * ____________ |
+ * <---- Direction of Search --- * | In Beam | Direction
+ * * | Closer | of Search
+ * * |___________| |
+ * * ____________ |
+ * * | focused |
+ * * | Item |
+ * * |___________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ ____________ ____________ * ____________
+ * | focused | | In Beam | | In Beam | * | focused |
+ * | Item | | Closer | | Farther | * | Item |
+ * |___________| |___________| |___________| * |___________| |
+ * * ____________ |
+ * ---- Direction of Search ---> * | In Beam | Direction
+ * * | Closer | of Search
+ * * |___________| |
+ * * ____________ v
+ * * | In Beam |
+ * * | Farther |
+ * * |___________|
+ */
+ @MediumTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam1() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 20, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 20)
+ FocusableBox(inBeamFarther, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 20, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 20, 20)
+ FocusableBox(inBeamFarther, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 20)
+ FocusableBox(inBeamFarther, 60, 0, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 20, 20)
+ FocusableBox(inBeamFarther, 0, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * | | ____________ *
+ * | In Beam | | | * ___________
+ * ____________ | Closer | | | * | In Beam |
+ * | In Beam | |___________| | focused | * | Farther |
+ * | Farther | | Item | * |__________| ^
+ * |___________| | | * _____________ |
+ * |___________| * | In Beam | Direction
+ * * | Closer | of Search
+ * <---- Direction of Search --- * |____________| |
+ * * __________________ |
+ * * | focused |
+ * * | Item |
+ * * |_________________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * __________________
+ * | | * | focused |
+ * | | ____________ * | Item |
+ * | focused | | In Beam | * |_________________| |
+ * | Item | ____________ | Farther | * _____________ |
+ * | | | | |___________| * | In Beam | Direction
+ * | | | In Beam | * | Closer | of Search
+ * |___________| | Closer | * |____________| |
+ * |___________| * ___________ v
+ * * | In Beam |
+ * ---- Direction of Search ---> * | Farther |
+ * * |__________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam2() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 10, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 30)
+ FocusableBox(inBeamFarther, 0, 20, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 20, 30, 30, 20)
+ FocusableBox(inBeamFarther, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 30)
+ FocusableBox(inBeamFarther, 60, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 20, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ ____________ * ___________
+ * ____________ | In Beam | | | * | In Beam |
+ * | In Beam | | Closer | | focused | * | Farther |
+ * | Farther | |___________| | Item | * |__________| ^
+ * |___________| | | * _____________ |
+ * |___________| * | In Beam | Direction
+ * * | Closer | of Search
+ * <---- Direction of Search --- * |____________| |
+ * * ___________________ |
+ * * | focused |
+ * * | Item |
+ * * |__________________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ___________________
+ * ____________ * | focused |
+ * | | ____________ * | Item |
+ * | focused | ____________ | In Beam | * |__________________| |
+ * | Item | | In Beam | | Farther | * _____________ |
+ * | | | Closer | |___________| * | In Beam | Direction
+ * |___________| |___________| * | Closer | of Search
+ * * |____________| |
+ * ---- Direction of Search ---> * ___________ v
+ * * | In Beam |
+ * * | Farther |
+ * * |__________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam3() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 20)
+ FocusableBox(inBeamFarther, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 20, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 20, 20, 20)
+ FocusableBox(inBeamFarther, 60, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 20, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ * ____________
+ * ____________ ____________ | | * | In Beam |
+ * | In Beam | | In Beam | | focused | * | Farther |
+ * | Farther | | Closer | | Item | * |___________| ^
+ * |___________| |___________| | | * ____________ |
+ * |___________| * | In Beam | Direction
+ * * | Closer | of Search
+ * <---- Direction of Search --- * |___________| |
+ * * ________________ |
+ * * | focused |
+ * * | Item |
+ * * |_______________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ________________
+ * ____________ * | focused |
+ * | | ____________ ____________ * | Item |
+ * | focused | | In Beam | | In Beam | * |_______________| |
+ * | Item | | Closer | | Farther | * ____________ |
+ * | | |___________| |___________| * | In Beam | Direction
+ * |___________| * | Closer | of Search
+ * * |___________| |
+ * ---- Direction of Search ---> * ____________ v
+ * * | In Beam |
+ * * | Farther |
+ * * |___________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam4() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 10, 20, 20)
+ FocusableBox(inBeamFarther, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 10, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 10, 20, 20)
+ FocusableBox(inBeamFarther, 60, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 10, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * } | * ___________
+ * ____________ | | * | In Beam |
+ * | In Beam | ____________ | focused | * | Farther |
+ * | Farther | | In Beam | | Item | * |__________| ^
+ * |___________| | Closer | | | * ____________ |
+ * |___________| |___________| * | In Beam | Direction
+ * * | Closer | of Search
+ * <---- Direction of Search --- * |___________| |
+ * * ____________________ |
+ * * | focused |
+ * * | Item |
+ * * |___________________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________________
+ * ____________ ____________ * | focused |
+ * | | | In Beam | ____________ * | Item |
+ * | focused | | Closer | | In Beam | * |___________________| |
+ * | Item | |___________| | Farther | * ____________ |
+ * | | |___________| * | In Beam | Direction
+ * | | * | Closer | of Search
+ * |___________| * |___________| |
+ * * ___________ v
+ * ---- Direction of Search ---> * | In Beam |
+ * * | Farther |
+ * * |__________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam5() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 20, 20, 20)
+ FocusableBox(inBeamFarther, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 0, 60, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 20, 20, 20)
+ FocusableBox(inBeamFarther, 60, 10, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 20, 20)
+ FocusableBox(inBeamFarther, 10, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * | | * ___________
+ * ____________ | | * | In Beam |
+ * | In Beam | | focused | * | Farther |
+ * | Farther | ____________ | Item | * |__________| ^
+ * |___________| | | | | * ____________ |
+ * | In Beam | | | * | In Beam | Direction
+ * | Closer | |___________| * | Closer | of Search
+ * |___________| * |___________| |
+ * * ___________________ |
+ * * | focused |
+ * <---- Direction of Search --- * | Item |
+ * * |__________________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ____________ * ___________________
+ * ____________ | | * | focused |
+ * | | | In Beam | ____________ * | Item |
+ * | focused | | Closer | | In Beam | * |__________________| |
+ * | Item | |___________| | Farther | * ____________ |
+ * | | |___________| * | In Beam | Direction
+ * | | * | Closer | of Search
+ * |___________| * |___________| |
+ * * ___________ v
+ * * | In Beam |
+ * ---- Direction of Search ---> * | Farther |
+ * * |__________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsWhenThereAreMultipleItemsInBeam6() {
+ // Arrange.
+ val (focusedItem, inBeamCloser, inBeamFarther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 0, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 20, 20, 30)
+ FocusableBox(inBeamFarther, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 10, 60, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 30, 20)
+ FocusableBox(inBeamFarther, 20, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 10, 20, 40, initialFocus)
+ FocusableBox(inBeamCloser, 30, 0, 20, 30)
+ FocusableBox(inBeamFarther, 60, 20, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 10, 0, 40, 20, initialFocus)
+ FocusableBox(inBeamCloser, 0, 30, 30, 20)
+ FocusableBox(inBeamFarther, 20, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(inBeamCloser.value).isTrue()
+ assertThat(inBeamFarther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ * ____________ ^
+ * | | * | | |
+ * ____________ | Closer | * | Farther | Direction
+ * | | |___________| * |___________| of Search
+ * | Farther | * ____________ |
+ * |___________| * | | |
+ * ____________ * | Closer |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | |
+ * ____________ * | | Closer |
+ * | | * | |___________|
+ * ____________ | Farther | * Direction ____________
+ * | | |___________| * of Search | |
+ * | Closer | * | | Farther |
+ * |___________| * v |___________|
+ */
+ @MediumTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam1() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 0, 20, 20)
+ FocusableBox(farther, 0, 10, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 0, 30, 20, 20)
+ FocusableBox(farther, 10, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 40, 20, 20)
+ FocusableBox(farther, 60, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 40, 30, 20, 20)
+ FocusableBox(farther, 30, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * * ____________ ^
+ * * | | |
+ * ____________ ____________ * | Farther | Direction
+ * | | | | * |___________| of Search
+ * | Farther | | Closer | * ____________ |
+ * |___________| |___________| * | | |
+ * ____________ * | Closer |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | |
+ * ____________ ____________ * | | Closer |
+ * | | | | * | |___________|
+ * | Closer | | Farther | * Direction ____________
+ * |___________| |___________| * of Search | |
+ * * | | Farther |
+ * * v |___________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam2() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 0, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 0, 30, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 60, 30, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 30, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ * ____________ ^
+ * | | * | | |
+ * | Farther | ____________ * | Farther | Direction
+ * |___________| | | * |___________| of Search
+ * | Closer | * ____________ |
+ * |___________| * | | |
+ * ____________ * | Closer |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | |
+ * ____________ * | | Closer |
+ * | | * | |___________|
+ * | Closer | ____________ * Direction ____________
+ * |___________| | | * of Search | |
+ * | Farther | * | | Farther |
+ * |___________| * v |___________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam3() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 10, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 10, 30, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 60, 40, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 40, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________
+ * | |
+ * | Farther | ^
+ * |___________| |
+ * ____________ Direction
+ * | | of Search
+ * | Closer | |
+ * |___________| |
+ * ____________
+ * | focused |
+ * | Item |
+ * |___________|
+ *
+ * <---- Direction of Search ---
+ *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ *
+ * ---- Direction of Search --->
+ * ____________
+ * | focused |
+ * | Item |
+ * |___________|
+ * ____________ |
+ * | | |
+ * | Closer | Direction
+ * |___________| of Search
+ * ____________ |
+ * | | v
+ * | Farther |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam4() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left, Up -> {
+ FocusableBox(focusedItem, 60, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Right, Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 60, 60, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * | | *
+ * | Farther | * ^
+ * |___________| * |
+ * ____________ * ____________ Direction
+ * | | * | | of Search
+ * | Closer | * | Farther | ____________ |
+ * |___________| * |___________| | | |
+ * ____________ * | Closer |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | |
+ * ____________ * | | Closer | ____________
+ * | | * | |___________| | |
+ * | Closer | * Direction | Farther |
+ * |___________| * of Search |___________|
+ * ____________ * |
+ * | | * v
+ * | Farther | *
+ * |___________| *
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam5() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 10, 30, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 10, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 40, 60, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 60, 40, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * | | *
+ * | Farther | * ^
+ * |___________| * |
+ * ____________ * Direction
+ * | | * of Search
+ * | Closer | * ____________ ____________ |
+ * |___________| * | | | | |
+ * ____________ * | Farther | | Closer |
+ * | focused | * |___________| |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________ ____________
+ * |___________| * | | | |
+ * ____________ * | | Closer | | Farther |
+ * | | * | |___________| |___________|
+ * | Closer | * Direction
+ * |___________| * of Search
+ * ____________ * |
+ * | | * v
+ * | Farther | *
+ * |___________| *
+ */
+ @LargeTest
+ @Test
+ fun closerItemWinsForItemsOutsideBeam6() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 30, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 0, 30, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 60, 30, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 0, 20, 20)
+ FocusableBox(farther, 0, 0, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 30, 60, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 30, 20, 20)
+ FocusableBox(farther, 60, 30, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isTrue()
+ assertThat(farther.value).isFalse()
+ }
+ }
+
+ /**
+ * ____________ *
+ * | | *
+ * | Farther | * ^
+ * |___________| * |
+ * ____________ * ____________ Direction
+ * | | * | | of Search
+ * | Closer | * ____________ | Closer | |
+ * |___________| * | | |___________| |
+ * ____________ * | Farther |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | focused |
+ * * | Item |
+ * <---- Direction of Search --- * |___________|
+ * *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * * ____________
+ * ---- Direction of Search ---> * | focused |
+ * ____________ * | Item |
+ * | focused | * |___________|
+ * | Item | * ____________
+ * |___________| * | |
+ * ____________ * | ____________ | Farther |
+ * | | * | | | |___________|
+ * | Closer | * Direction | Closer |
+ * |___________| * of Search |___________|
+ * ____________ * |
+ * | | * v
+ * | Farther | *
+ * |___________| *
+ */
+ @MediumTest
+ @Test
+ fun fartherItemWinsWhenTheMinorAxisDistanceIsMuchSmaller() {
+ // Arrange.
+ val (focusedItem, closer, farther) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(focusedItem, 40, 60, 20, 20, initialFocus)
+ FocusableBox(closer, 0, 30, 20, 20)
+ FocusableBox(farther, 10, 0, 20, 20)
+ }
+ Up -> {
+ FocusableBox(focusedItem, 60, 40, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 0, 20, 20)
+ FocusableBox(farther, 0, 10, 20, 20)
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 40, 30, 20, 20)
+ FocusableBox(farther, 30, 60, 20, 20)
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(closer, 30, 40, 20, 20)
+ FocusableBox(farther, 60, 30, 20, 20)
+ }
+ else -> error(invalid)
+ }
+ }
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(closer.value).isFalse()
+ assertThat(farther.value).isTrue()
+ }
+ }
+
+ private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
+ setContent {
+ focusManager = LocalFocusManager.current
+ composable()
+ }
+ rule.runOnIdle { initialFocus.requestFocus() }
+ }
+}
+
+@Composable
+private fun FocusableBox(
+ isFocused: MutableState<Boolean>,
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ focusRequester: FocusRequester? = null,
+ content: @Composable () -> Unit = {}
+) {
+ Layout(
+ content = content,
+ modifier = Modifier
+ .offset { IntOffset(x, y) }
+ .focusRequester(focusRequester ?: FocusRequester())
+ .onFocusChanged { isFocused.value = it.isFocused }
+ .focusModifier(),
+ measurePolicy = remember(width, height) {
+ MeasurePolicy { measurables, _ ->
+ val constraint = Constraints(width, width, height, height)
+ layout(width, height) {
+ measurables.forEach {
+ it.measure(constraint).place(0, 0)
+ }
+ }
+ }
+ }
+ )
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
new file mode 100644
index 0000000..04b5a7f
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusTraversalTwoItemsTest.kt
@@ -0,0 +1,4645 @@
+/*
+ * 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.focus
+
+import androidx.compose.foundation.layout.offset
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntOffset
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private const val invalid = "Not applicable to a 2D focus search."
+
+@RunWith(Parameterized::class)
+class TwoDimensionalFocusTraversalTwoItemsTest(private val focusDirection: FocusDirection) {
+ @get:Rule
+ val rule = createComposeRule()
+
+ private lateinit var focusManager: FocusManager
+ private val initialFocus: FocusRequester = FocusRequester()
+ private val focusedItem = mutableStateOf(false)
+ private val candidate = mutableStateOf(false)
+
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "direction={0}")
+ fun initParameters() = listOf(Left, Right, Up, Down)
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * |___________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun nonOverlappingCandidate1() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * |___________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate2() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * |___________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate3() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 20, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 20, 30, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate4() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 20, 10, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * |__________________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate5() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 30, 10)
+ FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate6() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate7() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 20, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate8() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 30, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * ________________ |_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate9() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 30, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * ________________ | candidate |
+ * | focusedItem | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate10() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 30, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up, Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * ________________ | |
+ * | focusedItem | | candidate |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate11() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 30, 0, 20, 20)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * ________________ | |
+ * | focusedItem | | candidate |
+ * |_______________| | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate12() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 0, 20, 30)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | | | candidate |
+ * | focusedItem | |_______________|
+ * | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate13() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 30, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | | ________________
+ * | focusedItem | | candidate |
+ * | | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate14() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 30, initialFocus)
+ FocusableBox(candidate, 30, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |
+ * | focusedItem | ________________
+ * | | | candidate |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate15() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 30, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | focusedItem | | candidate |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate16() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | focusedItem | | |
+ * |_______________| | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate17() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem | ________________
+ * |_______________| | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate18() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 5, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________| ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate19() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate20() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 30, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate21() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate22() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * _____________________
+ * | candidate |
+ * |____________________|
+ */
+ @MediumTest
+ @Test
+ fun nonOverlappingCandidate23() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 20, 30, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate24() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ______________
+ * | candidate |
+ * |_____________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate25() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+ FocusableBox(candidate, 10, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ____________
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate26() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+ FocusableBox(candidate, 10, 20, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ____________
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate27() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 30, 10, initialFocus)
+ FocusableBox(candidate, 0, 20, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * _____________________
+ * | candidate |
+ * |____________________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate28() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 20, 30, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Left, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun nonOverlappingCandidate29() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 20, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate30() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 20, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * ________________
+ * | candidate |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun nonOverlappingCandidate31() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 20, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * ________________ |_______________|
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate32() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * ________________ | focusedItem |
+ * | candidate | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate33() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | | | focusedItem |
+ * | candidate | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate34() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 15)
+ FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | candidate | | focusedItem |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate35() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________ ________________
+ * | candidate | | |
+ * |_______________| | focusedItem |
+ * | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate36() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * ________________ | |
+ * | candidate | | focusedItem |
+ * |_______________| | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate37() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 30, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |
+ * ________________ | focusedItem |
+ * | candidate | | |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate38() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 20, 10)
+ FocusableBox(focusedItem, 30, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | | ________________
+ * | candidate | | focusedItem |
+ * | | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate39() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 30)
+ FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | | ________________
+ * | candidate | | focusedItem |
+ * |_______________| |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate40() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Up, Down, Right -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate | ________________
+ * |_______________| | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate41() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 30, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________| ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate42() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 30, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate43() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 30, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate44() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 20, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate45() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * |__________________|
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun nonOverlappingCandidate46() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 30, 10)
+ FocusableBox(focusedItem, 10, 20, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * |___________|____
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary1() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * _|___________|__
+ * | focusedItem |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun candidateWithCommonBoundary2() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 5, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * ___|___________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary3() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary4() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * |__________________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary5() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 30, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * _______|_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary6() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * _______________|_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary7() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 20, 0, 20, 10)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________| candidate |
+ * | focusedItem |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary8() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________| |
+ * | focusedItem | candidate |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary9() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________| |
+ * | focusedItem | candidate |
+ * |_______________| |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun candidateWithCommonBoundary10() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | | candidate |
+ * | focusedItem |_______________|
+ * | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary11() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |________________
+ * | focusedItem | candidate |
+ * | |_______________|
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun candidateWithCommonBoundary12() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 20, 5, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |
+ * | focusedItem |________________
+ * | | candidate |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary13() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 20, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | focusedItem | candidate |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary14() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | focusedItem | |
+ * |_______________| candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary15() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |________________
+ * |_______________| candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary16() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 5, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|________________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary17() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 20, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|________
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary18() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|_____
+ * | candidate |
+ * |____________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary19() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 30, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary20() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * | candidate |
+ * |_____________| *
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary21() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 10, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary22() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 5, 10, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary23() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * ____|_______________|
+ * | candidate |
+ * |____________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary24() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 0, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * ______|_______________|
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary25() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * _______________|_______________|
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary26() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________| focusedItem |
+ * | candidate |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary27() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | | focusedItem |
+ * | candidate |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary28() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | candidate | focusedItem |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary29() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________________________
+ * | candidate | |
+ * |_______________| focusedItem |
+ * | |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary30() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________| |
+ * | candidate | focusedItem |
+ * |_______________| |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary31() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |
+ * _______________| focusedItem |
+ * | candidate | |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary32() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 20, 10)
+ FocusableBox(focusedItem, 20, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |________________
+ * | candidate | focusedItem |
+ * | |_______________|
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary33() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 20, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | |________________
+ * | candidate | focusedItem |
+ * |_______________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary34() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 20, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |________________
+ * |_______________| focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary35() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 20, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|________________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary36() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 20, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|________
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary37() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * |__________________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithCommonBoundary38() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * |___________|____
+ * |___________| |
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate1() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 10, 15)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * _|___________|__
+ * | |___________| |
+ * | focusedItem |
+ * |_______________|
+ */
+ @MediumTest
+ @Test
+ fun overlappingCandidate2() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 5, 0, 10, 15)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ____________
+ * | candidate |
+ * ___|___________|
+ * | |___________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate3() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 10, 15)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | candidate |
+ * |_______________|
+ * |_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate4() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 15)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * |________________ |
+ * |_______________|__|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate5() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 25, 15)
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * _______________|__ candidate |
+ * | focusedItem |__|_____________|
+ * |__________________|
+ */
+ @MediumTest
+ @Test
+ fun overlappingCandidate6() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________________
+ * _______________|___ |
+ * | focusedItem | | candidate |
+ * |_______________|__|______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate7() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 10, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * _________________
+ * _______________|___ |
+ * | focusedItem | | candidate |
+ * |_______________|__| |
+ * |________________|
+ */
+ @MediumTest
+ @Test
+ fun overlappingCandidate8() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 5, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * __________________________________
+ * | | | candidate |
+ * | focusedItem |__|______________|
+ * | |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate9() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | __|________________
+ * | focusedItem | | candidate |
+ * | |__|_______________|
+ * |__________________|
+ */
+ @MediumTest
+ @Test
+ fun overlappingCandidate10() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 10, 5, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | |
+ * | focusedItem __|________________
+ * | | | candidate |
+ * |_______________|__|_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate11() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 10, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________________________
+ * | focusedItem | | candidate |
+ * |_______________|__|_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate12() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________________________
+ * | focusedItem | | |
+ * |_______________|__| candidate |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate13() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | focusedItem __|________________
+ * |_______________|__| candidate |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate14() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 10, initialFocus)
+ FocusableBox(candidate, 10, 5, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Right, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|_____
+ * |_______________| |
+ * | candidate |
+ * |____________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate15() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+ FocusableBox(candidate, 0, 10, 30, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |_______________|
+ * |_______________|
+ * | candidate |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate16() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * | _____________|
+ * |_|_____________|
+ * | candidate |
+ * |_____________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate17() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ FocusableBox(candidate, 10, 10, 10, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * | ____________ |
+ * |_|___________|_|
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate18() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+ FocusableBox(candidate, 5, 10, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * |____________ |
+ * |___________|___|
+ * | candidate |
+ * |___________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate19() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 20, 15, initialFocus)
+ FocusableBox(candidate, 0, 10, 10, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ________________
+ * | focusedItem |
+ * ____|_______________|
+ * | |_______________|
+ * | candidate |
+ * |____________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate20() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 0, 10, 15, initialFocus)
+ FocusableBox(candidate, 0, 10, 20, 10)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * _______________|___ focusedItem |
+ * | candidate |__|_______________|
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate21() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Down -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________________________
+ * | | | focusedItem |
+ * | candidate |__|_______________|
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate22() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________________________
+ * | candidate | | focusedItem |
+ * |_______________|__|_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate23() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________________________
+ * | candidate | | |
+ * |_______________|__| focusedItem |
+ * | |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate24() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * _______________|___ |
+ * | candidate | | focusedItem |
+ * |_______________|__| |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate25() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | |
+ * _______________|___ focusedItem |
+ * | candidate | | |
+ * |_______________|__}_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate26() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 20, 10)
+ FocusableBox(focusedItem, 10, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | __|_________________
+ * | candidate | | focusedItem |
+ * | |__|________________|
+ * |__________________|
+ */
+ @MediumTest
+ @Test
+ fun overlappingCandidate27() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 10, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | __|_________________
+ * | candidate | | focusedItem |
+ * |_______________|__|________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate28() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 20)
+ FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Up, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate __|________________
+ * |_______________|__| focusedItem |
+ * |__________________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate29() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 5, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Left, Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ___________________
+ * | candidate |
+ * | _______________|
+ * |__|_______________|
+ * | focusedItem |
+ * |_______________|
+ */
+ @LargeTest
+ @Test
+ fun overlappingCandidate30() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 20, 10)
+ FocusableBox(focusedItem, 10, 5, 10, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ when (focusDirection) {
+ Up -> {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(candidate.value).isTrue()
+ }
+ Left, Right, Down -> {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ else -> error(invalid)
+ }
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | candidate | |
+ * |_____________| |
+ * | focusedItem |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem1() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | | candidate | |
+ * | |_____________| |
+ * | focusedItem |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem2() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 5, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | | candidate |
+ * | |_______________|
+ * | focusedItem |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem3() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | ______________|
+ * | focusedItem | candidate |
+ * | |______________|
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem4() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 5, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | focusedItem |
+ * | ______________|
+ * | | candidate |
+ * |______________|______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem5() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 10, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | focusedItem |
+ * | _______________ |
+ * | | candidate | |
+ * |_______|______________|______|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem6() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 5, 10, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | focusedItem |
+ * |______________ |
+ * | candidate | |
+ * |_____________|_______________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem7() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 10, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * |______________ |
+ * | candidate | focusedItem |
+ * |_____________| |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem8() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 0, 5, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | focusedItem |
+ * | ______________ |
+ * | | candidate | |
+ * | |_____________| |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun candidateWithinBoundsOfFocusedItem9() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 5, 5, 10, 10)
+ FocusableBox(focusedItem, 0, 0, 20, 20, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | focusedItem | |
+ * |_______________| |
+ * | candidate |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate1() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 0, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | | focusedItem | |
+ * | |_______________| |
+ * | candidate |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate2() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 5, 0, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | | focusedItem |
+ * | |_______________|
+ * | candidate |
+ * |_____________________________|
+ */
+ @MediumTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate3() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 0, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | _______________|
+ * | candidate | focusedItem |
+ * | |_______________|
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate4() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 5, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | candidate |
+ * | ________________|
+ * | | focusedItem |
+ * |____________|________________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate5() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 10, 10, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | candidate |
+ * | _______________ |
+ * | | focusedItem | |
+ * |_______|______________|______|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate6() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 5, 10, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | candidate |
+ * |_______________ |
+ * | focusedItem | |
+ * |______________|______________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate7() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 10, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * |________________ |
+ * | focusedItem | candidate |
+ * |_______________| |
+ * |_____________________________|
+ */
+ @LargeTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate8() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 0, 5, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ------------------------------
+ * | candidate |
+ * | _______________ |
+ * | | focusedItem | |
+ * | |______________| |
+ * |_____________________________|
+ */
+ @MediumTest
+ @Test
+ fun focusedItemWithinBoundsOfCandidate9() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(focusedItem, 5, 5, 10, 10, initialFocus)
+ FocusableBox(candidate, 0, 0, 20, 20)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ /**
+ * ______________________ * _________________
+ * _________________ | ________ | * | next sibling | ^
+ * | next sibling | | current | child | | * |________________| |
+ * |________________| | focus |_______| | * ______________________ Direction
+ * |_____________________| * | ________ | of Search
+ * * | current | child | | |
+ * <---- Direction of Search --- * | focus |_______| | |
+ * * |_____________________|
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * ______________________ * ______________________
+ * | ________ | _________________ * | ________ | |
+ * | current | child | | | next sibling | * | current | child | | |
+ * | focus |_______| | |________________| * | focus |_______| | Direction
+ * |_____________________| * |_____________________| of Search
+ * * _________________ |
+ * ---- Direction of Search ---> * | next sibling | |
+ * * |________________| v
+ */
+ @OptIn(ExperimentalComposeUiApi::class)
+ @MediumTest
+ @Test
+ fun picksSiblingAndNotChild() {
+ // Arrange.
+ val (focusedItem, child, nextSibling) = List(3) { mutableStateOf(false) }
+ rule.setContentForTest {
+ when (focusDirection) {
+ Left -> {
+ FocusableBox(nextSibling, 0, 10, 10, 10)
+ FocusableBox(focusedItem, 20, 0, 30, 30, initialFocus) {
+ FocusableBox(child, 10, 10, 10, 10)
+ }
+ }
+ Right -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus) {
+ FocusableBox(child, 10, 10, 10, 10)
+ }
+ FocusableBox(nextSibling, 40, 10, 10, 10)
+ }
+ Up -> {
+ FocusableBox(nextSibling, 10, 0, 10, 10)
+ FocusableBox(focusedItem, 0, 20, 30, 30, initialFocus) {
+ FocusableBox(child, 10, 10, 10, 10)
+ }
+ }
+ Down -> {
+ FocusableBox(focusedItem, 0, 0, 30, 30, initialFocus) {
+ FocusableBox(child, 10, 10, 10, 10)
+ }
+ FocusableBox(nextSibling, 10, 40, 10, 10)
+ }
+ else -> error(invalid)
+ }
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isFalse()
+ assertThat(child.value).isFalse()
+ assertThat(nextSibling.value).isTrue()
+ }
+ }
+
+ @MediumTest
+ @Test
+ fun sameBoundsForFocusedItemAndCandidate() {
+ // Arrange.
+ rule.setContentForTest {
+ FocusableBox(candidate, 10, 10, 20, 10)
+ FocusableBox(focusedItem, 10, 10, 20, 10, initialFocus)
+ }
+
+ // Act.
+ focusManager.moveFocus(focusDirection)
+
+ // Assert.
+ rule.runOnIdle {
+ assertThat(focusedItem.value).isTrue()
+ assertThat(candidate.value).isFalse()
+ }
+ }
+
+ private fun ComposeContentTestRule.setContentForTest(composable: @Composable () -> Unit) {
+ setContent {
+ focusManager = LocalFocusManager.current
+ composable()
+ }
+ rule.runOnIdle { initialFocus.requestFocus() }
+ }
+}
+
+@Composable
+private fun FocusableBox(
+ isFocused: MutableState<Boolean>,
+ x: Int,
+ y: Int,
+ width: Int,
+ height: Int,
+ focusRequester: FocusRequester? = null,
+ content: @Composable () -> Unit = {}
+) {
+ Layout(
+ content = content,
+ modifier = Modifier
+ .offset { IntOffset(x, y) }
+ .focusRequester(focusRequester ?: FocusRequester())
+ .onFocusChanged { isFocused.value = it.isFocused }
+ .focusModifier(),
+ measurePolicy = remember(width, height) {
+ MeasurePolicy { measurables, _ ->
+ val constraint = Constraints(width, width, height, height)
+ layout(width, height) {
+ measurables.forEach {
+ it.measure(constraint).place(0, 0)
+ }
+ }
+ }
+ }
+ )
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
index 71259f9..629dae5 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/AndroidPointerInputTest.kt
@@ -631,4 +631,4 @@
return findRootView(parent)
}
return view
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 2600605..1c82675 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -24,7 +24,6 @@
import android.view.MotionEvent.ACTION_POINTER_UP
import android.view.MotionEvent.ACTION_UP
import android.view.MotionEvent.TOOL_TYPE_UNKNOWN
-import androidx.activity.ComponentActivity
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
@@ -35,6 +34,7 @@
import androidx.compose.ui.platform.InspectableValue
import androidx.compose.ui.platform.ValueElement
import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.IntSize
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -50,7 +50,7 @@
@RunWith(AndroidJUnit4::class)
class PointerInteropFilterTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
private lateinit var pointerInteropFilter: PointerInteropFilter
private val dispatchedMotionEvents = mutableListOf<MotionEvent>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
index 04da750..81b70cd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/AlignmentLineTest.kt
@@ -16,14 +16,27 @@
package androidx.compose.ui.layout
+import androidx.activity.ComponentActivity
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.unit.Constraints
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import kotlin.math.min
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-@SmallTest
+@MediumTest
@RunWith(AndroidJUnit4::class)
class AlignmentLineTest {
+ @get:Rule
+ val rule = createAndroidComposeRule<ComponentActivity>()
+
@Test
fun queryingLinesOfUnmeasuredChild() {
val root = root {
@@ -39,4 +52,78 @@
createDelegate(root)
assertMeasuredAndLaidOut(root)
}
+
+ @Test
+ fun alignmentLinesPositionInCooperation_whenModifierDisobeys() {
+ val hLine = HorizontalAlignmentLine(::min)
+ val vLine = VerticalAlignmentLine(::min)
+ val hLinePosition = 50
+ val vLinePosition = 150
+ val constrainedSize = 100
+ val actualSize = 200
+ rule.setContent {
+ val contentWithAlignmentLines = @Composable {
+ Box(Modifier.requiredSize(with(rule.density) { actualSize.toDp() })) {
+ Layout({}, Modifier) { _, _ ->
+ layout(0, 0, mapOf(hLine to hLinePosition, vLine to vLinePosition)) {}
+ }
+ }
+ }
+ Layout(contentWithAlignmentLines) { measurables, _ ->
+ val placeable = measurables.first().measure(
+ Constraints(maxWidth = constrainedSize, maxHeight = constrainedSize)
+ )
+ val obtainedHLinePosition = placeable[hLine]
+ val obtainedVLinePosition = placeable[vLine]
+ assertEquals(
+ hLinePosition - (actualSize - constrainedSize) / 2,
+ obtainedHLinePosition
+ )
+ assertEquals(
+ vLinePosition - (actualSize - constrainedSize) / 2,
+ obtainedVLinePosition
+ )
+ layout(0, 0) {}
+ }
+ }
+ rule.waitForIdle()
+ }
+
+ @Test
+ fun alignmentLinesPositionInCooperation_whenLayoutDisobeys() {
+ val hLine = HorizontalAlignmentLine(::min)
+ val vLine = VerticalAlignmentLine(::min)
+ val hLinePosition = 50
+ val vLinePosition = 150
+ val constrainedSize = 100
+ val actualSize = 200
+ rule.setContent {
+ val contentWithAlignmentLines = @Composable {
+ Layout({}, Modifier) { _, _ ->
+ layout(
+ actualSize,
+ actualSize,
+ mapOf(hLine to hLinePosition, vLine to vLinePosition)
+ ) {}
+ }
+ }
+ Layout(contentWithAlignmentLines) { measurables, _ ->
+ val placeable = measurables.first().measure(
+ Constraints(maxWidth = constrainedSize, maxHeight = constrainedSize)
+ )
+ val obtainedHLinePosition = placeable[hLine]
+ val obtainedVLinePosition = placeable[vLine]
+ assertEquals(
+ hLinePosition - (actualSize - constrainedSize) / 2,
+ obtainedHLinePosition
+ )
+ assertEquals(
+ vLinePosition - (actualSize - constrainedSize) / 2,
+ obtainedVLinePosition
+ )
+ layout(0, 0) {}
+ }
+ }
+ rule.waitForIdle()
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index 2554407..3082620 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -20,7 +20,6 @@
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.ScrollView
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -45,6 +44,7 @@
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.unit.Constraints
@@ -69,7 +69,7 @@
class OnGloballyPositionedTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
@Test
fun handlesChildrenNodeMoveCorrectly() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index 595bcce..22e4869 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -18,13 +18,12 @@
import android.os.Build
import android.widget.FrameLayout
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.background
@@ -36,6 +35,7 @@
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.assertHeightIsEqualTo
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
@@ -65,7 +65,7 @@
class SubcomposeLayoutTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
@get:Rule
val excessiveAssertions = AndroidOwnerExtraAssertionsRule()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
index 50a283b..0c989fd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
@@ -24,6 +24,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.MonotonicFrameClock
import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.test.setViewLayerTypeForApi28
import androidx.core.view.doOnLayout
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -40,6 +41,7 @@
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
+import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,6 +52,11 @@
@get:Rule
val rule = activityScenarioRule<AppCompatActivity>()
+ @Before
+ fun setup() {
+ setViewLayerTypeForApi28()
+ }
+
@Test
fun currentThreadIsMainOnMainThread() = runBlocking(Dispatchers.Main) {
assertSame(AndroidUiDispatcher.Main, AndroidUiDispatcher.CurrentThread)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
index ef211d1..46dbe8b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardControllerTest.kt
@@ -16,10 +16,15 @@
package androidx.compose.ui.platform
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
@@ -29,6 +34,7 @@
import androidx.test.filters.LargeTest
import com.nhaarman.mockitokotlin2.inOrder
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import org.junit.Rule
@@ -43,7 +49,37 @@
@get:Rule
val rule = createComposeRule()
- @ExperimentalComposeUiApi
+ @Test
+ fun whenButtonClicked_performsHide_realisticAppTestCase() {
+ // code under test
+ @Composable
+ fun TestComposable() {
+ val softwareKeyboardController = LocalSoftwareKeyboardController.current
+ // Box instead of Button in this file for module dependency reasons
+ Box(Modifier.clickable { softwareKeyboardController?.hide() }) {
+ BasicText("Click Me")
+ }
+ }
+
+ // arrange
+ val mockSoftwareKeyboardController: SoftwareKeyboardController = mock()
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalSoftwareKeyboardController provides mockSoftwareKeyboardController
+ ) {
+ TestComposable()
+ }
+ }
+
+ // act
+ rule.onNodeWithText("Click Me").performClick()
+
+ // assert
+ rule.runOnIdle {
+ verify(mockSoftwareKeyboardController).hide()
+ }
+ }
+
@Test
fun localSoftwareKeybardController_delegatesTo_textInputService() {
val platformTextInputService = mock<PlatformTextInputService>()
@@ -55,7 +91,7 @@
) {
val controller = LocalSoftwareKeyboardController.current
SideEffect {
- controller?.hideSoftwareKeyboard()
+ controller?.hide()
}
}
}
@@ -84,8 +120,8 @@
rule.onNodeWithText("string").performClick()
rule.runOnIdle {
- controller?.hideSoftwareKeyboard()
- controller?.showSoftwareKeyboard()
+ controller?.hide()
+ controller?.show()
inOrder(platformTextInputService) {
verify(platformTextInputService).showSoftwareKeyboard() // focus
verify(platformTextInputService).hideSoftwareKeyboard() // explicit call
@@ -100,7 +136,29 @@
rule.setContent {
keyboardController = LocalSoftwareKeyboardController.current
}
- keyboardController!!.showSoftwareKeyboard()
- keyboardController!!.hideSoftwareKeyboard()
+ keyboardController!!.show()
+ keyboardController!!.hide()
+ }
+
+ @Test
+ fun showAndHide_noOp_whenProvidedMock() {
+ val mockSoftwareKeyboardController: SoftwareKeyboardController = mock()
+ val platformTextInputService = mock<PlatformTextInputService>()
+ val textInputService = TextInputService(platformTextInputService)
+ var controller: SoftwareKeyboardController? = null
+ rule.setContent {
+ CompositionLocalProvider(
+ LocalSoftwareKeyboardController provides mockSoftwareKeyboardController,
+ LocalTextInputService provides textInputService
+ ) {
+ controller = LocalSoftwareKeyboardController.current
+ }
+ }
+ rule.runOnIdle {
+ controller?.show()
+ controller?.hide()
+ }
+ verify(platformTextInputService, never()).hideSoftwareKeyboard()
+ verify(platformTextInputService, never()).showSoftwareKeyboard()
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
index 646f377..fe77ad6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowRecomposerTest.kt
@@ -28,7 +28,6 @@
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@@ -46,7 +45,7 @@
* Test that a Recomposer that doesn't shut down with the activity doesn't inadvertently
* keep a reference to the Activity
*/
- @OptIn(ExperimentalCoroutinesApi::class, InternalComposeUiApi::class)
+ @OptIn(InternalComposeUiApi::class)
@Test
@LargeTest
fun activityGarbageCollected() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
index c6da754..f7351f0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestActivity.kt
@@ -15,16 +15,22 @@
*/
package androidx.compose.ui.test
+import android.os.Build
import android.view.KeyEvent
import androidx.activity.ComponentActivity
+import androidx.compose.ui.platform.ViewLayer
import java.util.concurrent.CountDownLatch
-class TestActivity : ComponentActivity() {
+open class TestActivity : ComponentActivity() {
var receivedKeyEvent: KeyEvent? = null
var hasFocusLatch = CountDownLatch(1)
+ init {
+ setViewLayerTypeForApi28()
+ }
+
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
@@ -37,3 +43,15 @@
return super.onKeyDown(keyCode, event)
}
}
+
+/**
+ * We have a ViewLayer that doesn't use reflection that won't be activated on
+ * any Google devices, so we must trigger it directly. Here, we use it on all P
+ * devices. The normal ViewLayer is used on L devices. RenderNodeLayer is used
+ * on all other devices.
+ */
+internal fun setViewLayerTypeForApi28() {
+ if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
+ ViewLayer.shouldUseDispatchDraw = true
+ }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 8646e5d..db0b1eb 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -27,7 +27,6 @@
import android.widget.FrameLayout
import android.widget.RelativeLayout
import android.widget.TextView
-import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -50,6 +49,7 @@
import androidx.compose.ui.platform.findViewTreeCompositionContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.R
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -79,7 +79,7 @@
@RunWith(AndroidJUnit4::class)
class AndroidViewTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
@Test
fun androidViewWithConstructor() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
index fba2332..d849a82 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/EditTextInteropTest.kt
@@ -19,8 +19,8 @@
import android.view.KeyEvent
import android.view.View
import android.widget.EditText
-import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -33,7 +33,7 @@
@RunWith(AndroidJUnit4::class)
class EditTextInteropTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
@Test
fun hardwareKeyInEmbeddedView() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
index b799060..d37dd10 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils.kt
@@ -19,13 +19,13 @@
import android.os.Bundle
import android.view.View
import android.view.WindowManager
-import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.Owner
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
@@ -67,7 +67,7 @@
}
}
-internal class ActivityWithFlagSecure : ComponentActivity() {
+internal class ActivityWithFlagSecure : TestActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
index 3f678ca..ffd35e4 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PositionInWindowTest.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.TestActivity
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -41,7 +42,7 @@
class PositionInWindowTest {
@get:Rule
- val rule = createAndroidComposeRule<ComponentActivity>()
+ val rule = createAndroidComposeRule<TestActivity>()
lateinit var activity: ComponentActivity
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 844c3aa..232a9a6 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -89,7 +89,9 @@
import androidx.compose.ui.node.OwnerSnapshotObserver
import androidx.compose.ui.node.RootForTest
import androidx.compose.ui.semantics.SemanticsModifierCore
+import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.input.PlatformTextInputService
import androidx.compose.ui.text.input.TextInputService
@@ -100,7 +102,10 @@
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.trace
import androidx.compose.ui.viewinterop.AndroidViewHolder
+import androidx.core.view.AccessibilityDelegateCompat
import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.core.view.accessibility.AccessibilityNodeProviderCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewTreeLifecycleOwner
@@ -224,7 +229,7 @@
override var showLayoutBounds = false
private var _androidViewsHandler: AndroidViewsHandler? = null
- private val androidViewsHandler: AndroidViewsHandler
+ internal val androidViewsHandler: AndroidViewsHandler
get() {
if (_androidViewsHandler == null) {
_androidViewsHandler = AndroidViewsHandler(context)
@@ -232,9 +237,7 @@
}
return _androidViewsHandler!!
}
- private val viewLayersContainer by lazy(LazyThreadSafetyMode.NONE) {
- ViewLayerContainer(context).also { addView(it) }
- }
+ private var viewLayersContainer: DrawChildContainer? = null
// The constraints being used by the last onMeasure. It is set to null in onLayout. It allows
// us to detect the case when the View was measured twice with different constraints within
@@ -399,6 +402,30 @@
fun addAndroidView(view: AndroidViewHolder, layoutNode: LayoutNode) {
androidViewsHandler.holderToLayoutNode[view] = layoutNode
androidViewsHandler.addView(view)
+ androidViewsHandler.layoutNodeToHolder[layoutNode] = view
+ // Fetching AccessibilityNodeInfo from a View which is not set to
+ // IMPORTANT_FOR_ACCESSIBILITY_YES will return null.
+ ViewCompat.setImportantForAccessibility(
+ view,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
+ )
+ val thisView = this
+ ViewCompat.setAccessibilityDelegate(
+ view,
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View?,
+ info: AccessibilityNodeInfoCompat?
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ var parentId = SemanticsNode(layoutNode.outerSemantics!!, true).parent!!.id
+ if (parentId == semanticsOwner.rootSemanticsNode.id) {
+ parentId = AccessibilityNodeProviderCompat.HOST_VIEW_ID
+ }
+ info!!.setParent(thisView, parentId)
+ }
+ }
+ )
}
/**
@@ -408,6 +435,13 @@
fun removeAndroidView(view: AndroidViewHolder) {
androidViewsHandler.removeView(view)
androidViewsHandler.holderToLayoutNode.remove(view)
+ androidViewsHandler.layoutNodeToHolder.remove(
+ androidViewsHandler.holderToLayoutNode[view]
+ )
+ ViewCompat.setImportantForAccessibility(
+ view,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+ )
}
/**
@@ -547,12 +581,20 @@
isRenderNodeCompatible = false
}
}
- return ViewLayer(
- this,
- viewLayersContainer,
- drawBlock,
- invalidateParentLayer
- )
+ if (viewLayersContainer == null) {
+ if (!ViewLayer.hasRetrievedMethod) {
+ // Test to see if updateDisplayList() can be called. If this fails then
+ // ViewLayer.shouldUseDispatchDraw will be true.
+ ViewLayer.updateDisplayList(View(context))
+ }
+ if (ViewLayer.shouldUseDispatchDraw) {
+ viewLayersContainer = DrawChildContainer(context)
+ } else {
+ viewLayersContainer = ViewLayerContainer(context)
+ }
+ addView(viewLayersContainer)
+ }
+ return ViewLayer(this, viewLayersContainer!!, drawBlock, invalidateParentLayer)
}
override fun onSemanticsChange() {
@@ -577,6 +619,7 @@
invalidateLayers(root)
}
measureAndLayout()
+
// we don't have to observe here because the root has a layer modifier
// that will observe all children. The AndroidComposeView has only the
// root, so it doesn't have to invalidate itself based on model changes.
@@ -589,6 +632,17 @@
}
dirtyLayers.clear()
}
+
+ if (ViewLayer.shouldUseDispatchDraw) {
+ // We must update the display list of all children using dispatchDraw()
+ // instead of updateDisplayList(). But since we don't want to actually draw
+ // the contents, we will clip out everything from the canvas.
+ val saveCount = canvas.save()
+ canvas.clipRect(0f, 0f, 0f, 0f)
+
+ super.dispatchDraw(canvas)
+ canvas.restoreToCount(saveCount)
+ }
}
/**
@@ -791,6 +845,63 @@
return accessibilityDelegate.dispatchHoverEvent(event)
}
+ private fun findViewByAccessibilityIdRootedAtCurrentView(
+ accessibilityId: Int,
+ currentView: View
+ ): View? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ val getAccessibilityViewIdMethod = View::class.java
+ .getDeclaredMethod("getAccessibilityViewId")
+ getAccessibilityViewIdMethod.isAccessible = true
+ if (getAccessibilityViewIdMethod.invoke(currentView) == accessibilityId) {
+ return currentView
+ }
+ if (currentView is ViewGroup) {
+ for (i in 0 until currentView.childCount) {
+ val foundView = findViewByAccessibilityIdRootedAtCurrentView(
+ accessibilityId,
+ currentView.getChildAt(i)
+ )
+ if (foundView != null) {
+ return foundView
+ }
+ }
+ }
+ }
+ return null
+ }
+
+ /**
+ * This overrides an @hide method in ViewGroup. Because of the @hide, the override keyword
+ * cannot be used, but the override works anyway because the ViewGroup method is not final.
+ * In Android P and earlier, the call path is
+ * AccessibilityInteractionController#findViewByAccessibilityId ->
+ * View#findViewByAccessibilityId -> ViewGroup#findViewByAccessibilityIdTraversal. In Android
+ * Q and later, AccessibilityInteractionController#findViewByAccessibilityId uses
+ * AccessibilityNodeIdManager and findViewByAccessibilityIdTraversal is only used by autofill.
+ */
+ public fun findViewByAccessibilityIdTraversal(accessibilityId: Int): View? {
+ try {
+ // AccessibilityInteractionController#findViewByAccessibilityId doesn't call this
+ // method in Android Q and later. Ideally, we should only define this method in
+ // Android P and earlier, but since we don't have a way to do so, we can simply
+ // invoke the hidden parent method after Android P. If in new android, the hidden method
+ // ViewGroup#findViewByAccessibilityIdTraversal signature is changed or removed, we can
+ // simply return null here because there will be no call to this method.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val findViewByAccessibilityIdTraversalMethod = View::class.java
+ .getDeclaredMethod("findViewByAccessibilityIdTraversal", Int::class.java)
+ findViewByAccessibilityIdTraversalMethod.isAccessible = true
+ return findViewByAccessibilityIdTraversalMethod.invoke(this, accessibilityId) as?
+ View
+ } else {
+ return findViewByAccessibilityIdRootedAtCurrentView(accessibilityId, this)
+ }
+ } catch (e: NoSuchMethodException) {
+ return null
+ }
+ }
+
override val isLifecycleInResumedState: Boolean
get() = viewTreeOwners?.lifecycleOwner
?.lifecycle?.currentState == Lifecycle.State.RESUMED
@@ -958,4 +1069,4 @@
// not to add the default focus highlight to the whole compose view
view.defaultFocusHighlightEnabled = defaultFocusHighlightEnabled
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index a556129..cc8cdbe 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -154,7 +154,7 @@
}
/** Virtual view id for the currently hovered logical item. */
- private var hoveredVirtualViewId = InvalidId
+ internal var hoveredVirtualViewId = InvalidId
private val accessibilityManager: AccessibilityManager =
view.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
internal var accessibilityForceEnabledForTesting = false
@@ -296,7 +296,12 @@
semanticsNode.children.fastForEach { child ->
if (currentSemanticsNodes.contains(child.id)) {
- info.addChild(view, child.id)
+ val holder = view.androidViewsHandler.layoutNodeToHolder[child.layoutNode]
+ if (holder != null) {
+ info.addChild(holder)
+ } else {
+ info.addChild(view, child.id)
+ }
}
}
@@ -1203,16 +1208,36 @@
when (event.action) {
MotionEvent.ACTION_HOVER_MOVE, MotionEvent.ACTION_HOVER_ENTER -> {
- val virtualViewId: Int = getVirtualViewAt(event.getX(), event.getY())
+ val rootNode = view.semanticsOwner.rootSemanticsNode
+ val node = findSemanticsNodeAt(event.x, event.y, rootNode)
+ var virtualViewId = InvalidId
+ if (node != null) {
+ val hoveredView =
+ view.androidViewsHandler.layoutNodeToHolder[node.layoutNode]
+ if (hoveredView == null) {
+ virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(node.id)
+ }
+ }
+ // The android views could be view groups, so the event must be dispatched to the
+ // views. Android ViewGroup.java will take care of synthesizing hover enter/exit
+ // actions from hover moves.
+ // Note that this should be before calling "updateHoveredVirtualView" so that in
+ // the corner case of overlapped nodes, the final hover enter event is sent from
+ // the node/view that we want to focus.
+ val handled = view.androidViewsHandler.dispatchGenericMotionEvent(event)
updateHoveredVirtualView(virtualViewId)
- return (virtualViewId != InvalidId)
+ return if (virtualViewId == InvalidId) handled else true
}
MotionEvent.ACTION_HOVER_EXIT -> {
- if (hoveredVirtualViewId != InvalidId) {
- updateHoveredVirtualView(InvalidId)
- return true
+ return when {
+ hoveredVirtualViewId != InvalidId -> {
+ updateHoveredVirtualView(InvalidId)
+ true
+ }
+ else -> {
+ view.androidViewsHandler.dispatchGenericMotionEvent(event)
+ }
}
- return false
}
else -> {
return false
@@ -1220,36 +1245,27 @@
}
}
+ // TODO(b/151729467): compose accessibility findSemanticsNodeAt needs to be more efficient
+ /**
+ * Find the semantics node at the specified location. The location is relative to the root.
+ */
@VisibleForTesting
- internal fun getVirtualViewAt(x: Float, y: Float): Int {
- val node = view.semanticsOwner.rootSemanticsNode
- val id = findVirtualViewAt(
- x + node.boundsInWindow.left,
- y + node.boundsInWindow.top, node
- )
- if (id == node.id) {
- return AccessibilityNodeProviderCompat.HOST_VIEW_ID
- }
- return id
- }
-
- // TODO(b/151729467): compose accessibility getVirtualViewAt needs to be more efficient
- private fun findVirtualViewAt(x: Float, y: Float, node: SemanticsNode): Int {
+ internal fun findSemanticsNodeAt(x: Float, y: Float, node: SemanticsNode): SemanticsNode? {
val children = node.children
for (i in children.size - 1 downTo 0) {
- val id = findVirtualViewAt(x, y, children[i])
- if (id != InvalidId) {
- return id
+ val target = findSemanticsNodeAt(x, y, children[i])
+ if (target != null) {
+ return target
}
}
- if (node.boundsInWindow.left < x && node.boundsInWindow.right > x && node
- .boundsInWindow.top < y && node.boundsInWindow.bottom > y
+ if (node.boundsInRoot.left < x && node.boundsInRoot.right > x &&
+ node.boundsInRoot.top < y && node.boundsInRoot.bottom > y
) {
- return node.id
+ return node
}
- return InvalidId
+ return null
}
/**
@@ -1389,6 +1405,10 @@
if (!layoutNode.isAttached) {
return
}
+ // Android Views will send proper events themselves.
+ if (view.androidViewsHandler.layoutNodeToHolder.contains(layoutNode)) {
+ return
+ }
// When we finally send the event, make sure it is an accessibility-focusable node.
var semanticsWrapper = layoutNode.outerSemantics
?: layoutNode.findClosestParentNode { it.outerSemantics != null }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
index e72b4a2..5be4139 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
@@ -19,8 +19,6 @@
import android.os.Looper
import android.view.Choreographer
import androidx.compose.runtime.MonotonicFrameClock
-import androidx.compose.ui.platform.AndroidUiDispatcher.Companion.CurrentThread
-import androidx.compose.ui.platform.AndroidUiDispatcher.Companion.Main
import androidx.core.os.HandlerCompat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@@ -40,7 +38,6 @@
// not marked as async will adversely affect dispatch behavior but not to the point of
// incorrectness; more operations would be deferred to the choreographer frame as racing handler
// messages would wait behind a frame barrier.
-@OptIn(ExperimentalStdlibApi::class)
class AndroidUiDispatcher private constructor(
val choreographer: Choreographer,
private val handler: android.os.Handler
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
index 30b40a3..ace7c55 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidViewsHandler.android.kt
@@ -38,6 +38,7 @@
}
val holderToLayoutNode = hashMapOf<AndroidViewHolder, LayoutNode>()
+ val layoutNodeToHolder = hashMapOf<LayoutNode, AndroidViewHolder>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Layout will be handled by component nodes.
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 1844344..13238a3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -40,7 +40,7 @@
*/
internal class ViewLayer(
val ownerView: AndroidComposeView,
- val container: ViewLayerContainer,
+ val container: DrawChildContainer,
val drawBlock: (Canvas) -> Unit,
val invalidateParentLayer: () -> Unit
) : View(ownerView.context), OwnedLayer {
@@ -50,7 +50,8 @@
private var clipBoundsCache: android.graphics.Rect? = null
private val manualClipPath: Path? get() =
if (!clipToOutline) null else outlineResolver.clipPath
- private var isInvalidated = false
+ var isInvalidated = false
+ private set
private var drawnWithZ = false
private val canvasHolder = CanvasHolder()
@@ -221,13 +222,15 @@
}
override fun destroy() {
- container.removeView(this)
+ container.postOnAnimation {
+ container.removeView(this)
+ }
ownerView.dirtyLayers -= this
ownerView.requestClearInvalidObservations()
}
override fun updateDisplayList() {
- if (isInvalidated) {
+ if (isInvalidated && !shouldUseDispatchDraw) {
updateDisplayList(this)
isInvalidated = false
}
@@ -252,40 +255,48 @@
}
private var updateDisplayListIfDirtyMethod: Method? = null
private var recreateDisplayList: Field? = null
- private var hasRetrievedMethod = false
+ var hasRetrievedMethod = false
+ private set
+
+ var shouldUseDispatchDraw = false
+ internal set // internal so that tests can use it.
fun updateDisplayList(view: View) {
- if (!hasRetrievedMethod) {
- hasRetrievedMethod = true
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- updateDisplayListIfDirtyMethod =
- View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
- recreateDisplayList =
- View::class.java.getDeclaredField("mRecreateDisplayList")
- } else {
- val getDeclaredMethod = Class::class.java.getDeclaredMethod(
- "getDeclaredMethod",
- String::class.java,
- arrayOf<Class<*>>()::class.java
- )
- updateDisplayListIfDirtyMethod = getDeclaredMethod.invoke(
- View::class.java,
- "updateDisplayListIfDirty", emptyArray<Class<*>>()
- ) as Method?
- val getDeclaredField = Class::class.java.getDeclaredMethod(
- "getDeclaredField",
- String::class.java
- )
- recreateDisplayList = getDeclaredField.invoke(
- View::class.java,
- "mRecreateDisplayList"
- ) as Field?
+ try {
+ if (!hasRetrievedMethod) {
+ hasRetrievedMethod = true
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+ updateDisplayListIfDirtyMethod =
+ View::class.java.getDeclaredMethod("updateDisplayListIfDirty")
+ recreateDisplayList =
+ View::class.java.getDeclaredField("mRecreateDisplayList")
+ } else {
+ val getDeclaredMethod = Class::class.java.getDeclaredMethod(
+ "getDeclaredMethod",
+ String::class.java,
+ arrayOf<Class<*>>()::class.java
+ )
+ updateDisplayListIfDirtyMethod = getDeclaredMethod.invoke(
+ View::class.java,
+ "updateDisplayListIfDirty", emptyArray<Class<*>>()
+ ) as Method?
+ val getDeclaredField = Class::class.java.getDeclaredMethod(
+ "getDeclaredField",
+ String::class.java
+ )
+ recreateDisplayList = getDeclaredField.invoke(
+ View::class.java,
+ "mRecreateDisplayList"
+ ) as Field?
+ }
+ updateDisplayListIfDirtyMethod?.isAccessible = true
+ recreateDisplayList?.isAccessible = true
}
- updateDisplayListIfDirtyMethod?.isAccessible = true
- recreateDisplayList?.isAccessible = true
+ recreateDisplayList?.setBoolean(view, true)
+ updateDisplayListIfDirtyMethod?.invoke(view)
+ } catch (_: Throwable) {
+ shouldUseDispatchDraw = true
}
- recreateDisplayList?.setBoolean(view, true)
- updateDisplayListIfDirtyMethod?.invoke(view)
}
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
index 967bc00..4a3f22c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayerContainer.android.kt
@@ -25,21 +25,7 @@
/**
* The container we will use for [ViewLayer]s.
*/
-internal class ViewLayerContainer(context: Context) : ViewGroup(context) {
-
- init {
- clipChildren = false
- }
-
- override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
- // we don't layout our children
- }
-
- // we change visibility for this method so ViewLayer can use it for drawing
- internal fun drawChild(canvas: Canvas, view: View, drawingTime: Long) {
- super.drawChild(canvas.nativeCanvas, view, drawingTime)
- }
-
+internal class ViewLayerContainer(context: Context) : DrawChildContainer(context) {
override fun dispatchDraw(canvas: android.graphics.Canvas) {
// we draw our children as part of AndroidComposeView.dispatchDraw
}
@@ -49,5 +35,47 @@
* the display lists.
* We override hidden protected method from ViewGroup
*/
- protected fun dispatchGetDisplayList() {}
+ protected fun dispatchGetDisplayList() {
+ }
+}
+
+/**
+ * The container we will use for [ViewLayer]s when [ViewLayer.shouldUseDispatchDraw] is true.
+ */
+internal open class DrawChildContainer(context: Context) : ViewGroup(context) {
+ init {
+ clipChildren = false
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ // we don't layout our children
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // we don't measure our children
+ setMeasuredDimension(0, 0)
+ }
+
+ override fun dispatchDraw(canvas: android.graphics.Canvas) {
+ // We must updateDisplayListIfDirty for all invalidated Views.
+
+ // We only want to call super.dispatchDraw() if there is an invalidated layer
+ var doDispatch = false
+ for (i in 0 until childCount) {
+ val child = getChildAt(i) as ViewLayer
+ if (child.isInvalidated) {
+ doDispatch = true
+ break
+ }
+ }
+
+ if (doDispatch) {
+ super.dispatchDraw(canvas)
+ }
+ }
+
+ // we change visibility for this method so ViewLayer can use it for drawing
+ internal fun drawChild(canvas: Canvas, view: View, drawingTime: Long) {
+ super.drawChild(canvas.nativeCanvas, view, drawingTime)
+ }
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
index e0d7619..ccd2990 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/WindowRecomposer.android.kt
@@ -29,7 +29,6 @@
import androidx.lifecycle.ViewTreeLifecycleOwner
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.launch
@@ -204,7 +203,6 @@
}
}
-@OptIn(ExperimentalCoroutinesApi::class)
private fun View.createLifecycleAwareViewTreeRecomposer(): Recomposer {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
index 9a6108e..d56ac0c 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ColorResources.android.kt
@@ -19,6 +19,7 @@
import android.os.Build
import androidx.annotation.ColorRes
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@@ -29,6 +30,7 @@
* @return the color associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun colorResource(@ColorRes id: Int): Color {
val context = LocalContext.current
return if (Build.VERSION.SDK_INT >= 23) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
index f7cd48c..bc0a8aa 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/FontResources.android.kt
@@ -19,6 +19,7 @@
import android.content.Context
import androidx.annotation.GuardedBy
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.Typeface
import androidx.compose.ui.text.font.FontFamily
@@ -41,6 +42,7 @@
* @return the decoded image data associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun fontResource(fontFamily: FontFamily): Typeface {
return fontResourceFromContext(LocalContext.current, fontFamily)
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
index 6c2fb81..f10c3d9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PrimitiveResources.android.kt
@@ -21,6 +21,7 @@
import androidx.annotation.DimenRes
import androidx.annotation.IntegerRes
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
@@ -32,6 +33,7 @@
* @return the integer associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun integerResource(@IntegerRes id: Int): Int {
val context = LocalContext.current
return context.resources.getInteger(id)
@@ -44,6 +46,7 @@
* @return the integer array associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun integerArrayResource(@ArrayRes id: Int): IntArray {
val context = LocalContext.current
return context.resources.getIntArray(id)
@@ -56,6 +59,7 @@
* @return the boolean associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun booleanResource(@BoolRes id: Int): Boolean {
val context = LocalContext.current
return context.resources.getBoolean(id)
@@ -68,6 +72,7 @@
* @return the dimension value associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun dimensionResource(@DimenRes id: Int): Dp {
val context = LocalContext.current
val density = LocalDensity.current
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
index 70e41c5..189ee64 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/StringResources.android.kt
@@ -20,6 +20,7 @@
import androidx.annotation.ArrayRes
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@@ -30,6 +31,7 @@
* @return the string data associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun stringResource(@StringRes id: Int): String {
val resources = resources()
return resources.getString(id)
@@ -43,6 +45,7 @@
* @return the string data associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun stringResource(@StringRes id: Int, vararg formatArgs: Any): String {
val resources = resources()
return resources.getString(id, *formatArgs)
@@ -55,6 +58,7 @@
* @return the string data associated with the resource
*/
@Composable
+@ReadOnlyComposable
fun stringArrayResource(@ArrayRes id: Int): Array<String> {
val resources = resources()
return resources.getStringArray(id)
@@ -65,6 +69,7 @@
* gets updated.
*/
@Composable
+@ReadOnlyComposable
private fun resources(): Resources {
LocalConfiguration.current
return LocalContext.current.resources
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index a101327b..f88c289 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -28,7 +28,6 @@
import android.view.inputmethod.InputMethodManager
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.text.TextRange
-import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.Channel
import kotlin.math.roundToInt
@@ -158,7 +157,6 @@
showKeyboardChannel.offer(false)
}
- @OptIn(FlowPreview::class)
suspend fun keyboardVisibilityEventLoop() {
// TODO(b/180071033): Allow for more IMPLICIT flag to be passed.
for (showKeyboard in showKeyboardChannel) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 8f3aff5..7c2b07e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -32,6 +32,7 @@
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.LayoutDirection
/**
@@ -65,7 +66,11 @@
update: (T) -> Unit = NoOpUpdate
) {
val context = LocalContext.current
- val materialized = currentComposer.materialize(modifier)
+ // Create a semantics node for accessibility. Semantics modifier is composed and need to be
+ // materialized. So it can't be added in AndroidViewHolder when assigning modifier to layout
+ // node, which is after the materialize call.
+ val modifierWithSemantics = modifier.semantics(true) {}
+ val materialized = currentComposer.materialize(modifierWithSemantics)
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val parentReference = rememberCompositionContext()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 0112b21..7baab58 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -184,7 +184,7 @@
*/
val layoutNode: LayoutNode = run {
// TODO(soboleva): add layout direction here?
- // TODO(popam): forward pointer input, accessibility, focus
+ // TODO(popam): forward pointer input and focus
// Prepare layout node that proxies measure and layout passes to the View.
val layoutNode = LayoutNode()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
index 892199b..ba53f0f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
@@ -31,6 +31,8 @@
* Creates a [GraphicsLayerModifier] that draws the shadow. The [elevation] defines the visual
* depth of the physical object. The physical object has a shape specified by [shape].
*
+ * If the passed [shape] is concave the shadow will not be drawn on Android versions less than 10.
+ *
* Note that [elevation] is only affecting the shadow size and doesn't change the drawing order.
* Use [zIndex] modifier if you want to draw the elements with larger [elevation] after all the
* elements with a smaller one.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
index 9d55d7f..adf2c51 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusTraversal.kt
@@ -65,16 +65,18 @@
// If no custom focus traversal order is specified, perform a search for the appropriate item
// to move focus to.
- return when (focusDirection) {
- Next, Previous -> {
- // TODO(b/170155659): Perform one dimensional focus search.
- false
- }
- Left, Right, Up, Down -> {
- // TODO(b/170155926): Perform two dimensional focus search.
- false
- }
+ val nextNode = when (focusDirection) {
+ Next, Previous -> null // TODO(b/170155659): Perform one dimensional focus search.
+ Left, Right, Up, Down -> twoDimensionalFocusSearch(focusDirection)
}
+
+ // If we found a potential next item, call requestFocus() to move focus to it.
+ if (nextNode != null) {
+ nextNode.requestFocus(propagateFocus = false)
+ return true
+ }
+
+ return false
}
internal fun ModifiedFocusNode.findActiveFocusNode(): ModifiedFocusNode? {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
new file mode 100644
index 0000000..3cfb4f8
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/TwoDimensionalFocusSearch.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.focus
+
+import androidx.compose.ui.focus.FocusDirection.Down
+import androidx.compose.ui.focus.FocusDirection.Left
+import androidx.compose.ui.focus.FocusDirection.Right
+import androidx.compose.ui.focus.FocusDirection.Up
+import androidx.compose.ui.focus.FocusState.Active
+import androidx.compose.ui.focus.FocusState.ActiveParent
+import androidx.compose.ui.focus.FocusState.Captured
+import androidx.compose.ui.focus.FocusState.Disabled
+import androidx.compose.ui.focus.FocusState.Inactive
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.ModifiedFocusNode
+import androidx.compose.ui.util.fastForEach
+import kotlin.math.absoluteValue
+import kotlin.math.max
+
+private const val invalidFocusDirection = "This function should only be used for 2-D focus search"
+
+/**
+ * Perform a search among the immediate children of this [node][ModifiedFocusNode] in the
+ * specified [direction][FocusDirection] and return the node that is to be focused next. If one
+ * of the children is currently focused, we start from that point and search in the specified
+ * [direction][FocusDirection]. If none of the children are currently focused, we pick the
+ * top-left or bottom right based on the specified [direction][FocusDirection].
+ */
+internal fun ModifiedFocusNode.twoDimensionalFocusSearch(
+ direction: FocusDirection
+): ModifiedFocusNode? {
+ return when (focusState) {
+ Inactive -> this
+ Disabled -> null
+ ActiveParent -> {
+ val focusedChild = focusedChild
+ checkNotNull(focusedChild) { "ActiveParent must have a focusedChild" }
+
+ // If this node contains the focused child, pick one of the other children.
+ // Otherwise, this is an intermediate parent. Continue searching among the children of
+ // the child that has focus.
+ if (focusedChild.focusState.isFocused) {
+ focusableChildren().findBestCandidate(focusedChild.focusRect(), direction)
+ } else {
+ focusedChild.twoDimensionalFocusSearch(direction)
+ }
+ }
+ Active, Captured -> {
+ // The 2-D focus search starts form the root. If we reached here, it means that there
+ // was no intermediate node that was ActiveParent. This is an initial focus scenario.
+ // We need to search among this node's children to find the best focus candidate.
+ val focusableChildren = focusableChildren()
+
+ // If there are aren't multiple children to choose from, return the first child.
+ if (focusableChildren.size <= 1) {
+ return focusableChildren.firstOrNull()
+ }
+
+ // To start the search, we pick one of the four corners of this node as the initially
+ // focused rectangle.
+ val initialFocusRect = when (direction) {
+ Right, Down -> focusRect().topLeft()
+ Left, Up -> focusRect().bottomRight()
+ else -> error(invalidFocusDirection)
+ }
+ focusableChildren.findBestCandidate(initialFocusRect, direction)
+ }
+ }
+}
+
+// Iterate through this list of focus nodes and find best candidate in the specified direction.
+// TODO(b/182319711): For Left/Right focus moves, Consider finding the first candidate in the beam
+// and then only comparing candidates in the beam. If nothing is in the beam, then consider all
+// valid candidates.
+private fun List<ModifiedFocusNode>.findBestCandidate(
+ focusRect: Rect,
+ direction: FocusDirection
+): ModifiedFocusNode? {
+ // Pick an impossible rectangle as the initial best candidate Rect.
+ var bestCandidate = when (direction) {
+ Left -> focusRect.translate(focusRect.width + 1, 0f)
+ Right -> focusRect.translate(-(focusRect.width + 1), 0f)
+ Up -> focusRect.translate(0f, focusRect.height + 1)
+ Down -> focusRect.translate(0f, -(focusRect.height + 1))
+ else -> error(invalidFocusDirection)
+ }
+
+ var searchResult: ModifiedFocusNode? = null
+ fastForEach { candidateNode ->
+ val candidateRect = candidateNode.focusRect()
+ if (isBetterCandidate(candidateRect, bestCandidate, focusRect, direction)) {
+ bestCandidate = candidateRect
+ searchResult = candidateNode
+ }
+ }
+ return searchResult
+}
+
+// Is this Rect a better candidate than currentCandidateRect for a focus search in a particular
+// direction from a source rect? This is the core routine that determines the order of focus
+// searching.
+private fun isBetterCandidate(
+ proposedCandidate: Rect,
+ currentCandidate: Rect,
+ focusedRect: Rect,
+ direction: FocusDirection
+): Boolean {
+
+ // Is this Rect a candidate for the next focus given the direction? This checks whether the
+ // rect is at least partially to the direction of (e.g left of) from source. Includes an edge
+ // case for an empty rect (which is used in some cases when searching from a point on the
+ // screen).
+ fun Rect.isCandidate() = when (direction) {
+ Left -> (focusedRect.right > right || focusedRect.left >= right) && focusedRect.left > left
+ Right -> (focusedRect.left < left || focusedRect.right <= left) && focusedRect.right < right
+ Up -> (focusedRect.bottom > bottom || focusedRect.top >= bottom) && focusedRect.top > top
+ Down -> (focusedRect.top < top || focusedRect.bottom <= top) && focusedRect.bottom < bottom
+ else -> error(invalidFocusDirection)
+ }
+
+ // The distance from the edge furthest in the given direction of source to the edge nearest
+ // in the given direction of dest. If the dest is not in the direction from source, return 0.
+ fun Rect.majorAxisDistance(): Float {
+ val majorAxisDistance = when (direction) {
+ Left -> focusedRect.left - right
+ Right -> left - focusedRect.right
+ Up -> focusedRect.top - bottom
+ Down -> top - focusedRect.bottom
+ else -> error(invalidFocusDirection)
+ }
+ return max(0.0f, majorAxisDistance)
+ }
+
+ // Find the distance on the minor axis w.r.t the direction to the nearest edge of the
+ // destination rectangle.
+ fun Rect.minorAxisDistance() = when (direction) {
+ // the distance between the center verticals
+ Left, Right -> (focusedRect.top + focusedRect.height / 2) - (top + height / 2)
+ // the distance between the center horizontals
+ Up, Down -> (focusedRect.left + focusedRect.width / 2) - (left + width / 2)
+ else -> error(invalidFocusDirection)
+ }
+
+ // Fudge-factor opportunity: how to calculate distance given major and minor axis distances.
+ // Warning: This fudge factor is finely tuned, run all focus tests if you dare tweak it.
+ fun weightedDistance(candidate: Rect): Long {
+ val majorAxisDistance = candidate.majorAxisDistance().absoluteValue.toLong()
+ val minorAxisDistance = candidate.minorAxisDistance().absoluteValue.toLong()
+ return 13 * majorAxisDistance * majorAxisDistance + minorAxisDistance * minorAxisDistance
+ }
+
+ return when {
+ // to be a better candidate, need to at least be a candidate in the first place.
+ !proposedCandidate.isCandidate() -> false
+
+ // If the currentCandidate is not a candidate, proposedCandidate is better.
+ !currentCandidate.isCandidate() -> true
+
+ // if proposedCandidate is better by beam, it wins.
+ beamBeats(focusedRect, proposedCandidate, currentCandidate, direction) -> true
+
+ // if currentCandidate is better, then the proposedCandidate can't be.
+ beamBeats(focusedRect, currentCandidate, proposedCandidate, direction) -> false
+
+ else -> weightedDistance(proposedCandidate) < weightedDistance(currentCandidate)
+ }
+}
+
+/**
+ * A rectangle may be a better candidate by virtue of being exclusively in the beam of the source
+ * rect.
+ * @return Whether rect1 is a better candidate than rect2 by virtue of it being in the source's
+ * beam.
+ */
+private fun beamBeats(source: Rect, rect1: Rect, rect2: Rect, direction: FocusDirection): Boolean {
+ // Do the "beams" w.r.t the given direction's axis of rect1 and rect2 overlap?
+ fun Rect.inSourceBeam() = when (direction) {
+ Left, Right -> this.bottom > source.top && this.top < source.bottom
+ Up, Down -> this.right > source.left && this.left < source.right
+ else -> error(invalidFocusDirection)
+ }
+
+ // Whether the rect is in the direction of search.
+ fun Rect.isInDirectionOfSearch() = when (direction) {
+ Left -> source.left >= right
+ Right -> source.right <= left
+ Up -> source.top >= bottom
+ Down -> source.bottom <= top
+ else -> error(invalidFocusDirection)
+ }
+
+ // The distance from the edge furthest in the given direction of source to the edge nearest
+ // in the given direction of dest. If the dest is not in the direction from source, return 0.
+ fun Rect.majorAxisDistance(): Float {
+ val majorAxisDistance = when (direction) {
+ Left -> source.left - right
+ Right -> left - source.right
+ Up -> source.top - bottom
+ Down -> top - source.bottom
+ else -> error(invalidFocusDirection)
+ }
+ return max(0.0f, majorAxisDistance)
+ }
+
+ // The distance along the major axis w.r.t the direction from the edge of source to the far
+ // edge of dest. If the dest is not in the direction from source, return 1 (to break ties
+ // with Rect.majorAxisDistance).
+ fun Rect.majorAxisDistanceToFarEdge(): Float {
+ val majorAxisDistance = when (direction) {
+ Left -> source.left - left
+ Right -> right - source.right
+ Up -> source.top - top
+ Down -> bottom - source.bottom
+ else -> error(invalidFocusDirection)
+ }
+ return max(1.0f, majorAxisDistance)
+ }
+
+ return when {
+ // if rect1 isn't exclusively in the src beam, it doesn't win
+ rect2.inSourceBeam() || !rect1.inSourceBeam() -> false
+
+ // We know rect1 is in the beam, and rect2 is not. If Rect2 is not in the direction of
+ // search, rect1 wins (since rect2 could be reached by going in another direction).
+ !rect2.isInDirectionOfSearch() -> true
+
+ // for horizontal directions, being exclusively in beam always wins
+ direction == Left || direction == Right -> true
+
+ // for vertical directions, beams only beat up to a point:
+ // now, as long as rect2 isn't completely closer, rect1 wins
+ // e.g for direction down, completely closer means for rect2's top
+ // edge to be closer to the source's top edge than rect1's bottom edge.
+ else -> rect1.majorAxisDistance() < rect2.majorAxisDistanceToFarEdge()
+ }
+}
+
+private fun Rect.topLeft() = Rect(left, top, left, top)
+private fun Rect.bottomRight() = Rect(right, bottom, right, bottom)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index 626f9d8..fe604ab 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -37,6 +37,9 @@
* rotation ([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
* ([shadowElevation], [shape]), and clipping ([clip], [shape]).
*
+ * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave the
+ * shadow will not be drawn on Android versions less than 10.
+ *
* If the layer parameters are backed by a [androidx.compose.runtime.State] or an animated value
* prefer an overload with a lambda block on [GraphicsLayerScope] as reading a state inside the block
* will only cause the layer properties update without triggering recomposition and relayout.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
index 4285fad..b81db67 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
@@ -62,6 +62,9 @@
* Sets the elevation for the shadow in pixels. With the [shadowElevation] > 0f and
* [shape] set, a shadow is produced. Default value is `0` and the value must not be
* negative.
+ *
+ * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave the
+ * shadow will not be drawn on Android versions less than 10.
*/
/*@setparam:FloatRange(from = 0.0)*/
var shadowElevation: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
index 9d13b37..bb6f2a9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/AnimatedImageVector.kt
@@ -31,14 +31,14 @@
*
* @param imageVector The [ImageVector] to be animated. This is represented with the
* `android:drawable` parameter of an `<animated-vector>` element.
- * @param targets The list of [AnimatedVectorTarget]s that specify animations for each of the
- * elements in the drawable. This is represented with `<target>` elements in `<animated-vector>`.
- * This list is expected to be *immutable*.
*/
@ExperimentalComposeUiApi
@Immutable
class AnimatedImageVector internal constructor(
val imageVector: ImageVector,
+ // The list of [AnimatedVectorTarget]s that specify animations for each of the elements in the
+ // drawable. This is represented with `<target>` elements in `<animated-vector>`. This list is
+ // expected to be *immutable*.
internal val targets: List<AnimatedVectorTarget>
) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
index 4640ac6..593831d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/AlignmentLine.kt
@@ -49,8 +49,6 @@
*
* @see VerticalAlignmentLine
* @see HorizontalAlignmentLine
- *
- * @param merger Defines the position of an alignment line inherited from more than one child.
*/
@Immutable
sealed class AlignmentLine(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index ebbc0b2..f4d9abd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -97,7 +97,7 @@
/**
* The constraints used for the measurement made to obtain this [Placeable].
*/
- protected var measurementConstraints: Constraints = Constraints()
+ protected var measurementConstraints: Constraints = DefaultConstraints
/**
* The offset to be added to an apparent position assigned to this [Placeable] to make it real.
@@ -330,3 +330,5 @@
* Block on [GraphicsLayerScope] which applies the default layer parameters.
*/
private val DefaultLayerBlock: GraphicsLayerScope.() -> Unit = {}
+
+private val DefaultConstraints = Constraints()
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
index 9e961e0..387da83 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingLayoutNodeWrapper.kt
@@ -80,7 +80,7 @@
}
}
- override fun get(alignmentLine: AlignmentLine): Int = wrapped[alignmentLine]
+ override fun calculateAlignmentLine(alignmentLine: AlignmentLine) = wrapped[alignmentLine]
override fun placeAt(
position: IntOffset,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index f6a580b..a8b4970 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -108,7 +108,7 @@
layoutNode.onNodePlaced()
}
- override operator fun get(alignmentLine: AlignmentLine): Int {
+ override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
return layoutNode.calculateAlignmentLines()[alignmentLine] ?: AlignmentLine.Unspecified
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index ea36814..d153075 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.input.pointer.PointerInputFilter
import androidx.compose.ui.input.pointer.PointerInputModifier
import androidx.compose.ui.layout.AlignmentLine
-import androidx.compose.ui.layout.HorizontalAlignmentLine
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
@@ -48,7 +47,6 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
-import androidx.compose.ui.layout.merge
import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut
import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring
import androidx.compose.ui.node.LayoutNode.LayoutState.NeedsRelayout
@@ -62,7 +60,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
-import kotlin.math.roundToInt
/**
* Enable to log changes to the LayoutNode tree. This logging is quite chatty.
@@ -102,17 +99,20 @@
// the list of nodes where the virtual children are unfolded (their children are represented
// as our direct children)
- private val _unfoldedChildren = mutableVectorOf<LayoutNode>()
+ private var _unfoldedChildren: MutableVector<LayoutNode>? = null
private fun recreateUnfoldedChildrenIfDirty() {
if (unfoldedVirtualChildrenListDirty) {
unfoldedVirtualChildrenListDirty = false
- _unfoldedChildren.clear()
+ val unfoldedChildren = _unfoldedChildren ?: mutableVectorOf<LayoutNode>().also {
+ _unfoldedChildren = it
+ }
+ unfoldedChildren.clear()
_foldedChildren.forEach {
if (it.isVirtual) {
- _unfoldedChildren.addAll(it._children)
+ unfoldedChildren.addAll(it._children)
} else {
- _unfoldedChildren.add(it)
+ unfoldedChildren.add(it)
}
}
}
@@ -135,7 +135,7 @@
_foldedChildren
} else {
recreateUnfoldedChildrenIfDirty()
- _unfoldedChildren
+ _unfoldedChildren!!
}
/**
@@ -491,12 +491,13 @@
/**
* The alignment lines of this layout, inherited + intrinsic
*/
- internal val alignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+ internal var alignmentLines: LayoutNodeAlignmentLines? = null
+ private set
/**
* The alignment lines provided by this layout at the last measurement
*/
- internal val providedAlignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+ internal var providedAlignmentLines: Map<AlignmentLine, Int> = emptyMap()
internal val mDrawScope: LayoutNodeDrawScope = sharedDrawScope
@@ -558,8 +559,6 @@
internal var alignmentUsageByParent = UsageByParent.NotUsed
- private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
-
@Deprecated("Temporary API to support ConstraintLayout prototyping.")
internal var canMultiMeasure: Boolean = false
@@ -638,13 +637,17 @@
owner!!.onSemanticsChange()
}
val addedCallback = hasNewPositioningCallback()
- onPositionedCallbacks.clear()
+ onPositionedCallbacks?.clear()
// Create a new chain of LayoutNodeWrappers, reusing existing ones from wrappers
// when possible.
val outerWrapper = modifier.foldOut(innerLayoutNodeWrapper) { mod, toWrap ->
var wrapper = toWrap
if (mod is OnGloballyPositionedModifier) {
+ val onPositionedCallbacks = onPositionedCallbacks
+ ?: mutableVectorOf<OnGloballyPositionedModifier>().also {
+ onPositionedCallbacks = it
+ }
onPositionedCallbacks += mod
}
if (mod is RemeasurementModifier) {
@@ -763,7 +766,7 @@
/**
* List of all OnPositioned callbacks in the modifier chain.
*/
- private val onPositionedCallbacks = mutableVectorOf<OnGloballyPositionedModifier>()
+ private var onPositionedCallbacks: MutableVector<OnGloballyPositionedModifier>? = null
/**
* Flag used by [OnPositionedDispatcher] to identify LayoutNodes that have already
@@ -812,30 +815,11 @@
}
/**
- * Returns the alignment line value for a given alignment line without affecting whether
- * the flag for whether the alignment line was read.
- */
- internal fun getAlignmentLine(alignmentLine: AlignmentLine): Int? {
- val linePos = alignmentLines[alignmentLine] ?: return null
- var pos = Offset(linePos.toFloat(), linePos.toFloat())
- var wrapper = innerLayoutNodeWrapper
- while (wrapper != outerLayoutNodeWrapper) {
- pos = wrapper.toParentPosition(pos)
- wrapper = wrapper.wrappedBy!!
- }
- pos = wrapper.toParentPosition(pos)
- return if (alignmentLine is HorizontalAlignmentLine) {
- pos.y.roundToInt()
- } else {
- pos.x.roundToInt()
- }
- }
-
- /**
* Return true if there is a new [OnGloballyPositionedModifier] assigned to this Layout.
*/
private fun hasNewPositioningCallback(): Boolean {
- return modifier.foldOut(false) { mod, hasNewCallback ->
+ val onPositionedCallbacks = onPositionedCallbacks
+ return onPositionedCallbacks != null && modifier.foldOut(false) { mod, hasNewCallback ->
hasNewCallback ||
(mod is OnGloballyPositionedModifier && mod !in onPositionedCallbacks)
}
@@ -930,28 +914,10 @@
alignmentLinesCalculatedDuringLastLayout = false
if (alignmentLinesRequired) {
alignmentLinesCalculatedDuringLastLayout = true
- previousAlignmentLines.clear()
- previousAlignmentLines.putAll(alignmentLines)
- alignmentLines.clear()
- _children.forEach { child ->
- if (!child.isPlaced) return@forEach
- child.alignmentLines.keys.forEach { childLine ->
- val linePositionInContainer = child.getAlignmentLine(childLine)!!
- // If the line was already provided by a previous child, merge the values.
- alignmentLines[childLine] = if (childLine in alignmentLines) {
- childLine.merge(
- alignmentLines.getValue(childLine),
- linePositionInContainer
- )
- } else {
- linePositionInContainer
- }
- }
+ val alignments = alignmentLines ?: LayoutNodeAlignmentLines(this).also {
+ alignmentLines = it
}
- alignmentLines += providedAlignmentLines
- if (previousAlignmentLines != alignmentLines) {
- onAlignmentsChanged()
- }
+ alignments.recalculate()
}
layoutState = Ready
}
@@ -1055,13 +1021,12 @@
layoutState = endState
}
isCalculatingAlignmentLines = false
- return alignmentLines
+ return alignmentLines?.getLastCalculation() ?: emptyMap()
}
internal fun handleMeasureResult(measureResult: MeasureResult) {
innerLayoutNodeWrapper.measureResult = measureResult
- this.providedAlignmentLines.clear()
- this.providedAlignmentLines += measureResult.alignmentLines
+ providedAlignmentLines = measureResult.alignmentLines
}
/**
@@ -1093,7 +1058,7 @@
if (!isPlaced) {
return // it hasn't been placed, so don't make a call
}
- onPositionedCallbacks.forEach { it.onGloballyPositioned(coordinates) }
+ onPositionedCallbacks?.forEach { it.onGloballyPositioned(coordinates) }
}
/**
@@ -1185,8 +1150,17 @@
}
modifier.foldIn(Unit) { _, mod ->
- val wrapper = wrapperCache.firstOrNull { it.modifier === mod }
- wrapper?.toBeReusedForSameModifier = true
+ var wrapper = wrapperCache.lastOrNull {
+ it.modifier === mod && !it.toBeReusedForSameModifier
+ }
+ // we want to walk up the chain up all LayoutNodeWrappers for the same modifier
+ while (wrapper != null) {
+ wrapper.toBeReusedForSameModifier = true
+ wrapper = if (wrapper.isChained)
+ wrapper.wrappedBy as? DelegatingLayoutNodeWrapper<*>
+ else
+ null
+ }
}
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
new file mode 100644
index 0000000..fba763a
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -0,0 +1,82 @@
+/*
+ * 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.node
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.HorizontalAlignmentLine
+import androidx.compose.ui.layout.merge
+import kotlin.math.roundToInt
+
+internal class LayoutNodeAlignmentLines(
+ private val layoutNode: LayoutNode
+) {
+ /**
+ * The alignment lines of this layout, inherited + intrinsic
+ */
+ private val alignmentLines: MutableMap<AlignmentLine, Int> = hashMapOf()
+
+ private val previousAlignmentLines = mutableMapOf<AlignmentLine, Int>()
+
+ fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLines
+
+ fun recalculate() {
+ previousAlignmentLines.clear()
+ previousAlignmentLines.putAll(alignmentLines)
+ alignmentLines.clear()
+ layoutNode._children.forEach { child ->
+ val childAlignments = child.alignmentLines
+ if (!child.isPlaced || childAlignments == null) return@forEach
+ childAlignments.alignmentLines.keys.forEach { childLine ->
+ val linePositionInContainer = childAlignments.getAlignmentLine(childLine)!!
+ // If the line was already provided by a previous child, merge the values.
+ alignmentLines[childLine] = if (childLine in alignmentLines) {
+ childLine.merge(
+ alignmentLines.getValue(childLine),
+ linePositionInContainer
+ )
+ } else {
+ linePositionInContainer
+ }
+ }
+ }
+ alignmentLines += layoutNode.providedAlignmentLines
+ if (previousAlignmentLines != alignmentLines) {
+ layoutNode.onAlignmentsChanged()
+ }
+ }
+
+ /**
+ * Returns the alignment line value for a given alignment line without affecting whether
+ * the flag for whether the alignment line was read.
+ */
+ private fun getAlignmentLine(alignmentLine: AlignmentLine): Int? {
+ val linePos = alignmentLines[alignmentLine] ?: return null
+ var pos = Offset(linePos.toFloat(), linePos.toFloat())
+ var wrapper = layoutNode.innerLayoutNodeWrapper
+ while (wrapper != layoutNode.outerLayoutNodeWrapper) {
+ pos = wrapper.toParentPosition(pos)
+ wrapper = wrapper.wrappedBy!!
+ }
+ pos = wrapper.toParentPosition(pos)
+ return if (alignmentLine is HorizontalAlignmentLine) {
+ pos.y.roundToInt()
+ } else {
+ pos.x.roundToInt()
+ }
+ }
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index fa5252f..9958594 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -31,11 +31,13 @@
import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
import androidx.compose.ui.input.nestedscroll.NestedScrollDelegatingWrapper
import androidx.compose.ui.input.pointer.PointerInputFilter
+import androidx.compose.ui.layout.AlignmentLine
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.VerticalAlignmentLine
import androidx.compose.ui.layout.findRoot
import androidx.compose.ui.layout.positionInRoot
import androidx.compose.ui.unit.Constraints
@@ -164,6 +166,18 @@
return result
}
+ abstract fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int
+
+ final override fun get(alignmentLine: AlignmentLine): Int {
+ val measuredPosition = calculateAlignmentLine(alignmentLine)
+ if (measuredPosition == AlignmentLine.Unspecified) return AlignmentLine.Unspecified
+ return measuredPosition + if (alignmentLine is VerticalAlignmentLine) {
+ apparentToRealOffset.x
+ } else {
+ apparentToRealOffset.y
+ }
+ }
+
/**
* Places the modified child.
*/
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index 18c4ca3..9004a6a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -27,6 +27,7 @@
import androidx.compose.ui.focus.findFocusableChildren
import androidx.compose.ui.focus.searchChildrenForFocusNode
import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.util.fastForEach
internal class ModifiedFocusNode(
@@ -45,22 +46,16 @@
sendOnFocusEvent(value)
}
- // TODO(b/175900268): Add API to allow a parent to extends the bounds of the focus Modifier.
- // For now we just use the bounds of this node.
- val focusRect: Rect
- get() = Rect(
- left = position.x.toFloat(),
- top = position.y.toFloat(),
- right = size.width.toFloat(),
- bottom = size.height.toFloat()
- )
-
var focusedChild: ModifiedFocusNode?
get() = modifier.focusedChild
set(value) {
modifier.focusedChild = value
}
+ // TODO(b/175900268): Add API to allow a parent to extends the bounds of the focus Modifier.
+ // For now we just use the bounds of this node.
+ fun focusRect(): Rect = boundsInRoot()
+
// TODO(b/152051577): Measure the performance of focusableChildren.
// Consider caching the children.
fun focusableChildren(): List<ModifiedFocusNode> {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
index 614d548..bcf5547 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedLayoutNode.kt
@@ -59,7 +59,7 @@
measureScope.maxIntrinsicHeight(wrapped, width)
}
- override operator fun get(alignmentLine: AlignmentLine): Int {
+ override fun calculateAlignmentLine(alignmentLine: AlignmentLine): Int {
if (measureResult.alignmentLines.containsKey(alignmentLine)) {
return measureResult.alignmentLines[alignmentLine] ?: AlignmentLine.Unspecified
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
index 4f2e9c5..eadbc1c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
@@ -38,7 +38,7 @@
}
private var lastPosition: IntOffset = IntOffset.Zero
private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
- private val lastProvidedAlignmentLines = mutableMapOf<AlignmentLine, Int>()
+ private var lastProvidedAlignmentLines: MutableMap<AlignmentLine, Int>? = null
private var lastZIndex: Float = 0f
/**
@@ -90,16 +90,12 @@
measuredOnce = true
layoutNode.layoutState = LayoutState.Measuring
measurementConstraints = constraints
- lastProvidedAlignmentLines.clear()
- lastProvidedAlignmentLines.putAll(layoutNode.providedAlignmentLines)
val outerWrapperPreviousMeasuredSize = outerWrapper.size
owner.snapshotObserver.observeMeasureSnapshotReads(layoutNode) {
outerWrapper.measure(constraints)
}
layoutNode.layoutState = LayoutState.NeedsRelayout
- if (layoutNode.providedAlignmentLines != lastProvidedAlignmentLines) {
- layoutNode.onAlignmentsChanged()
- }
+ notifyAlignmentChanges()
val sizeChanged = outerWrapper.size != outerWrapperPreviousMeasuredSize ||
outerWrapper.width != width ||
outerWrapper.height != height
@@ -110,6 +106,26 @@
return false
}
+ private fun notifyAlignmentChanges() {
+ // optimized to only create a lastProvidedAlignmentLines when we do have non empty map
+ if (layoutNode.providedAlignmentLines.isNotEmpty()) {
+ val previous = lastProvidedAlignmentLines ?: mutableMapOf<AlignmentLine, Int>().also {
+ lastProvidedAlignmentLines = it
+ }
+ if (layoutNode.providedAlignmentLines != previous) {
+ previous.clear()
+ previous.putAll(layoutNode.providedAlignmentLines)
+ layoutNode.onAlignmentsChanged()
+ }
+ } else {
+ val previous = lastProvidedAlignmentLines
+ if (previous != null && previous.isNotEmpty()) {
+ previous.clear()
+ layoutNode.onAlignmentsChanged()
+ }
+ }
+ }
+
// We are setting our measuredSize to match the coerced outerWrapper size, to prevent
// double offseting for layout cooperation. However, this means that here we need
// to override these getters to make the measured values correct in Measured.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
index 312783f..dbcc400 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/LocalSoftwareKeyboardController.kt
@@ -17,6 +17,8 @@
package androidx.compose.ui.platform
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ProvidedValue
+import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.text.input.TextInputService
@@ -24,31 +26,51 @@
@ExperimentalComposeUiApi
public object LocalSoftwareKeyboardController {
+ private val LocalSoftwareKeyboardController =
+ compositionLocalOf<SoftwareKeyboardController?> { null }
+
/**
- * Return a [SoftwareKeyboardController] that delegates to the current [LocalTextInputService].
+ * Return a [SoftwareKeyboardController] that can control the current software keyboard.
*
- * Returns null if there is no [LocalTextInputService] and the software keyboard cannot be
- * controlled.
+ * If it is not provided, the default implementation will delegate to [LocalTextInputService].
+ *
+ * Returns null if the software keyboard cannot be controlled.
*/
@ExperimentalComposeUiApi
public val current: SoftwareKeyboardController?
@Composable get() {
- val textInputService = LocalTextInputService.current ?: return null
- return remember(textInputService) {
- DelegatingSotwareKeyboardController(textInputService)
- }
+ return LocalSoftwareKeyboardController.current ?: delegatingController()
}
+
+ @Composable
+ private fun delegatingController(): SoftwareKeyboardController? {
+ val textInputService = LocalTextInputService.current ?: return null
+ return remember(textInputService) {
+ DelegatingSoftwareKeyboardController(textInputService)
+ }
+ }
+
+ /**
+ * Set the key [LocalSoftwareKeyboardController] in [CompositionLocalProvider].
+ */
+ public infix fun provides(
+ softwareKeyboardController: SoftwareKeyboardController
+ ): ProvidedValue<SoftwareKeyboardController?> {
+ return LocalSoftwareKeyboardController.provides(softwareKeyboardController)
+ }
}
@ExperimentalComposeUiApi
-private class DelegatingSotwareKeyboardController(
- val textInputService: TextInputService?
+private class DelegatingSoftwareKeyboardController(
+ val textInputService: TextInputService
) : SoftwareKeyboardController {
- override fun showSoftwareKeyboard() {
- textInputService?.showSoftwareKeyboard()
+ override fun show() {
+ @Suppress("DEPRECATION")
+ textInputService.showSoftwareKeyboard()
}
- override fun hideSoftwareKeyboard() {
- textInputService?.hideSoftwareKeyboard()
+ override fun hide() {
+ @Suppress("DEPRECATION")
+ textInputService.hideSoftwareKeyboard()
}
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
index 5150781..339684b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/SoftwareKeyboardController.kt
@@ -16,12 +16,14 @@
package androidx.compose.ui.platform
+import androidx.compose.runtime.Stable
import androidx.compose.ui.ExperimentalComposeUiApi
/**
* Provide software keyboard control.
*/
@ExperimentalComposeUiApi
+@Stable
interface SoftwareKeyboardController {
/**
* Request that the system show a software keyboard.
@@ -37,13 +39,22 @@
*
* @sample androidx.compose.ui.samples.SoftwareKeyboardControllerSample
*
- * You do not need to call this function unless you also call [hideSoftwareKeyboard], as the
+ * You do not need to call this function unless you also call [hide], as the
* keyboard is automatically shown and hidden by focus events in the BasicTextField.
*
* Calling this function is considered a side-effect and should not be called directly from
* recomposition.
*/
- fun showSoftwareKeyboard()
+ fun show()
+
+ /**
+ * @see show
+ */
+ @Deprecated(
+ "Use show instead.",
+ ReplaceWith("show()")
+ )
+ fun showSoftwareKeyboard() = show()
/**
* Hide the software keyboard.
@@ -56,5 +67,14 @@
* Calling this function is considered a side-effect and should not be called directly from
* recomposition.
*/
- fun hideSoftwareKeyboard()
+ fun hide()
+
+ /**
+ * @see hide
+ */
+ @Deprecated(
+ "Use hide instead.",
+ ReplaceWith("hide()")
+ )
+ fun hideSoftwareKeyboard() = hide()
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
index dcfd076..874e013 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.desktop.kt
@@ -116,12 +116,14 @@
defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
addWindowListener(object : WindowAdapter() {
override fun windowClosing(event: WindowEvent) {
- if (defaultCloseOperation != WindowConstants.DO_NOTHING_ON_CLOSE) {
- onDispose?.invoke()
- onDismiss?.invoke()
- events.invokeOnClose()
- AppManager.removeWindow(parent)
+ if (!isClosed) {
isClosed = true
+ if (defaultCloseOperation != WindowConstants.DO_NOTHING_ON_CLOSE) {
+ onDispose?.invoke()
+ onDismiss?.invoke()
+ events.invokeOnClose()
+ AppManager.removeWindow(parent)
+ }
}
}
override fun windowIconified(event: WindowEvent) {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
index 25b5672..92cfd5c 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.desktop.kt
@@ -161,6 +161,17 @@
event
)
}
+
+ override fun mouseEntered(event: MouseEvent) = events.post {
+ owners.onMouseEntered(
+ (event.x * density.density).toInt(),
+ (event.y * density.density).toInt()
+ )
+ }
+
+ override fun mouseExited(event: MouseEvent) = events.post {
+ owners.onMouseExited()
+ }
})
wrapped.addMouseMotionListener(object : MouseMotionAdapter() {
override fun mouseDragged(event: MouseEvent) = events.post {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
index 08be312..90c25cf 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.desktop.kt
@@ -18,6 +18,7 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionContext
import org.jetbrains.skiko.ClipComponent
+import org.jetbrains.skiko.GraphicsApi
import java.awt.Component
import javax.swing.JFrame
import javax.swing.JLayeredPane
@@ -28,6 +29,7 @@
* @param parent The parent AppFrame that wraps the ComposeWindow.
*/
class ComposeWindow(val parent: AppFrame) : JFrame() {
+ private var isDisposed = false
internal val layer = ComposeLayer()
private val pane = object : JLayeredPane() {
override fun setBounds(x: Int, y: Int, width: Int, height: Int) {
@@ -76,7 +78,10 @@
}
override fun dispose() {
- layer.dispose()
+ if (!isDisposed) {
+ layer.dispose()
+ isDisposed = true
+ }
super.dispose()
}
@@ -86,4 +91,18 @@
layer.component.requestFocus()
}
}
+
+ /**
+ * Retrieve underlying platform-specific operating system handle for the window where ComposeWindow is rendered.
+ * Currently returns HWND on Windows, Drawable on X11 and 0 on macOS.
+ */
+ val windowHandle: Long
+ get() = layer.component.windowHandle
+
+ /**
+ * Returns low level rendering API used for rendering in this ComposeWindow. API is automatically selected based on
+ * operating system, graphical hardware and `SKIKO_RENDER_API` environment variable.
+ */
+ val renderApi: GraphicsApi
+ get() = layer.component.renderApi
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
index b496be2..02572ed 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.desktop.kt
@@ -358,4 +358,34 @@
oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
newMoveFilters = mutableListOf()
}
+
+ internal fun onPointerEnter(position: Offset) {
+ var onEnterConsumed = false
+ // TODO: do we actually need that?
+ measureAndLayout()
+ root.hitTest(position, newMoveFilters)
+ for (
+ filter in newMoveFilters
+ .asReversed()
+ .asSequence()
+ .filterIsInstance<PointerMoveEventFilter>()
+ ) {
+ if (!onEnterConsumed) {
+ onEnterConsumed = filter.onEnterHandler()
+ }
+ }
+ oldMoveFilters = newMoveFilters.filterIsInstance<PointerMoveEventFilter>()
+ newMoveFilters = mutableListOf()
+ }
+
+ internal fun onPointerExit() {
+ var onExitConsumed = false
+ for (filter in oldMoveFilters.asReversed()) {
+ if (!onExitConsumed) {
+ onExitConsumed = filter.onExitHandler()
+ }
+ }
+ oldMoveFilters = listOf()
+ newMoveFilters = mutableListOf()
+ }
}
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 2ab89f4..31b9743 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
@@ -27,7 +27,6 @@
import androidx.compose.ui.input.pointer.PointerType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import org.jetbrains.skija.Canvas
import java.awt.event.InputMethodEvent
@@ -39,7 +38,6 @@
error("CompositionLocal DesktopOwnersAmbient not provided")
}
-@OptIn(ExperimentalCoroutinesApi::class)
internal class DesktopOwners(
coroutineScope: CoroutineScope,
component: DesktopComponent = DummyDesktopComponent,
@@ -81,8 +79,6 @@
internal val platformInputService: DesktopPlatformInput = DesktopPlatformInput(component)
init {
- // TODO(demin): Experimental API (CoroutineStart.UNDISPATCHED).
- // Decide what to do before release (copy paste or use different approach).
coroutineScope.launch(coroutineContext, start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
@@ -153,6 +149,15 @@
lastOwner?.onPointerMove(position)
}
+ fun onMouseEntered(x: Int, y: Int) {
+ val position = Offset(x.toFloat(), y.toFloat())
+ lastOwner?.onPointerEnter(position)
+ }
+
+ fun onMouseExited() {
+ lastOwner?.onPointerExit()
+ }
+
private fun consumeKeyEvent(event: KeyEvent) {
list.lastOrNull()?.sendKeyEvent(ComposeKeyEvent(event))
}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
index 9cc2205..a4c5c67 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt
@@ -85,11 +85,9 @@
}
override fun showSoftwareKeyboard() {
- println("DesktopPlatformInput.showSoftwareKeyboard")
}
override fun hideSoftwareKeyboard() {
- println("DesktopPlatformInput.hideSoftwareKeyboard")
}
override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
@@ -109,7 +107,6 @@
) {
// Which OSes and which input method could produce such events? We need to have some
// specific cases in mind before implementing this
- println("DesktopInputComponent.inputMethodCaretPositionChanged")
}
internal fun replaceInputMethodText(event: InputMethodEvent) {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
index 28603b8..fb2f66b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/TestComposeWindow.desktop.kt
@@ -121,4 +121,25 @@
fun onMouseScroll(x: Int, y: Int, event: MouseScrollEvent) {
owners.onMouseScroll(x, y, event)
}
+
+ /**
+ * Process mouse move event
+ */
+ fun onMouseMoved(x: Int, y: Int) {
+ owners.onMouseMoved(x, y)
+ }
+
+ /**
+ * Process mouse enter event
+ */
+ fun onMouseEntered(x: Int, y: Int) {
+ owners.onMouseEntered(x, y)
+ }
+
+ /**
+ * Process mouse exit event
+ */
+ fun onMouseExited() {
+ owners.onMouseExited()
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt
new file mode 100644
index 0000000..0793543
--- /dev/null
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/input/mouse/MouseHoverFilterTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.input.mouse
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerMoveFilter
+import androidx.compose.ui.platform.TestComposeWindow
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MouseHoverFilterTest {
+ private val window = TestComposeWindow(width = 100, height = 100, density = Density(2f))
+
+ @Test
+ fun `inside window`() {
+ var moveCount = 0
+ var enterCount = 0
+ var exitCount = 0
+
+ window.setContent {
+ Box(
+ modifier = Modifier
+ .pointerMoveFilter(
+ onMove = {
+ moveCount++
+ false
+ },
+ onEnter = {
+ enterCount++
+ false
+ },
+ onExit = {
+ exitCount++
+ false
+ }
+ )
+ .size(10.dp, 20.dp)
+ )
+ }
+
+ window.onMouseMoved(
+ x = 10,
+ y = 20
+ )
+ assertThat(enterCount).isEqualTo(1)
+ assertThat(exitCount).isEqualTo(0)
+ assertThat(moveCount).isEqualTo(1)
+
+ window.onMouseMoved(
+ x = 10,
+ y = 15
+ )
+ assertThat(enterCount).isEqualTo(1)
+ assertThat(exitCount).isEqualTo(0)
+ assertThat(moveCount).isEqualTo(2)
+
+ window.onMouseMoved(
+ x = 30,
+ y = 30
+ )
+ assertThat(enterCount).isEqualTo(1)
+ assertThat(exitCount).isEqualTo(1)
+ assertThat(moveCount).isEqualTo(2)
+ }
+
+ @Test
+ fun `window enter`() {
+ var moveCount = 0
+ var enterCount = 0
+ var exitCount = 0
+
+ window.setContent {
+ Box(
+ modifier = Modifier
+ .pointerMoveFilter(
+ onMove = {
+ moveCount++
+ false
+ },
+ onEnter = {
+ enterCount++
+ false
+ },
+ onExit = {
+ exitCount++
+ false
+ }
+ )
+ .size(10.dp, 20.dp)
+ )
+ }
+
+ window.onMouseEntered(
+ x = 10,
+ y = 20
+ )
+ assertThat(enterCount).isEqualTo(1)
+ assertThat(exitCount).isEqualTo(0)
+ assertThat(moveCount).isEqualTo(0)
+
+ window.onMouseExited()
+ assertThat(enterCount).isEqualTo(1)
+ assertThat(exitCount).isEqualTo(1)
+ assertThat(moveCount).isEqualTo(0)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index aba2aab..802806b 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -48,14 +48,11 @@
import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
import androidx.compose.ui.unit.dp
import com.google.common.truth.Truth
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assert.assertFalse
+import org.junit.Assume.assumeTrue
import org.junit.Rule
import org.junit.Test
-@OptIn(
- ExperimentalCoroutinesApi::class
-)
class DesktopOwnerTest {
@get:Rule
val screenshotRule = DesktopScreenshotTestRule("ui/ui-desktop/ui")
@@ -283,6 +280,8 @@
height = 40,
platform = DesktopPlatform.Windows // scrolling behave differently on different platforms
) {
+ // Disabled for now, as LazyColumn behaves slightly differently than test.
+ assumeTrue(false)
var height by mutableStateOf(10.dp)
setContent {
Box(Modifier.padding(10.dp)) {
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index 66e3e5e..434aec0 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -23,7 +23,6 @@
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.swing.Swing
@@ -33,7 +32,6 @@
import org.jetbrains.skiko.FrameDispatcher
import kotlin.coroutines.CoroutineContext
-@OptIn(ExperimentalCoroutinesApi::class)
internal fun renderingTest(
width: Int,
height: Int,
diff --git a/compose/ui/ui/src/proguard-rules.pro b/compose/ui/ui/src/proguard-rules.pro
index da6e00d..45df3a1 100644
--- a/compose/ui/ui/src/proguard-rules.pro
+++ b/compose/ui/ui/src/proguard-rules.pro
@@ -17,3 +17,7 @@
# R8 to complain about them not being there during optimization.
-dontwarn android.view.RenderNode
-dontwarn android.view.DisplayListCanvas
+
+-keepclassmember class androidx.compose.ui.platform.ViewLayerContainer {
+ protected void dispatchGetDisplayList();
+}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 5b0bf3d..41517fc 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -1681,6 +1681,33 @@
assertEquals(2, owner.layoutChangeCount)
}
+ @Test
+ fun reuseModifiersThatImplementMultipleModifierInterfaces() {
+ val drawAndLayoutModifier: Modifier = object : DrawModifier, LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.placeRelative(IntOffset.Zero)
+ }
+ }
+ override fun ContentDrawScope.draw() {
+ drawContent()
+ }
+ }
+ val a = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
+ val b = Modifier.then(EmptyLayoutModifier()).then(drawAndLayoutModifier)
+ val node = LayoutNode(20, 20, 100, 100)
+ val owner = MockOwner()
+ node.attach(owner)
+ node.modifier = a
+ assertEquals(3, node.getModifierInfo().size)
+ node.modifier = b
+ assertEquals(3, node.getModifierInfo().size)
+ }
+
private fun createSimpleLayout(): Triple<LayoutNode, LayoutNode, LayoutNode> {
val layoutNode = ZeroSizedLayoutNode()
val child1 = ZeroSizedLayoutNode()
@@ -1696,6 +1723,18 @@
PointerInputModifier
}
+private class EmptyLayoutModifier : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ placeable.placeRelative(IntOffset.Zero)
+ }
+ }
+}
+
@OptIn(InternalCoreApi::class)
private class MockOwner(
val position: IntOffset = IntOffset.Zero,
diff --git a/core/core-google-shortcuts/build.gradle b/core/core-google-shortcuts/build.gradle
index cc7820c..ba29bf6 100644
--- a/core/core-google-shortcuts/build.gradle
+++ b/core/core-google-shortcuts/build.gradle
@@ -33,7 +33,7 @@
dependencies {
api(KOTLIN_STDLIB)
- api(project(":core:core"))
+ api("androidx.core:core:1.6.0-alpha01")
implementation("com.google.firebase:firebase-appindexing:19.2.0")
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 1caa816..dc42557 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
@@ -22,7 +22,9 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -36,6 +38,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.google.android.gms.tasks.Task;
+import com.google.android.gms.tasks.Tasks;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.firebase.appindexing.Action;
@@ -77,6 +81,7 @@
@SmallTest
public void onShortcutUpdated_publicIntent_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+ when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
Intent intent = Intent.parseUri("http://www.google.com", 0);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -104,8 +109,61 @@
@Test
@SmallTest
+ public void onShortcutAdded_updateSuccess_reportUsage() throws Exception {
+ ArgumentCaptor<Action> actionCaptor = ArgumentCaptor.forClass(Action.class);
+ Task<Void> result = Tasks.forResult(null);
+ when(mFirebaseAppIndex.update(any())).thenReturn(result);
+
+ Intent intent = Intent.parseUri("http://www.google.com", 0);
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+ .setShortLabel("short label")
+ .setLongLabel("long label")
+ .setIntent(intent)
+ .setIcon(IconCompat.createWithContentUri("content://abc"))
+ .build();
+
+ mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
+
+ // Sleep to make sure the asynchronous call finishes. Since the method is mocked this
+ // should be really quick.
+ Thread.sleep(100);
+ verify(mFirebaseUserActions).end(actionCaptor.capture());
+
+ Action action = actionCaptor.getValue();
+ Action expectedAction = new Action.Builder(Action.Builder.VIEW_ACTION)
+ .setObject("", ShortcutUtils.getIndexableUrl(mContext, "publicIntent"))
+ .setMetadata(new Action.Metadata.Builder().setUpload(false))
+ .build();
+ assertThat(action.toString()).isEqualTo(expectedAction.toString());
+ }
+
+ @Test
+ @SmallTest
+ public void onShortcutAdded_updateError_doNotReportUsage() throws Exception {
+ when(mFirebaseAppIndex.update(any()))
+ .thenReturn(Tasks.forException(new RuntimeException()));
+
+ Intent intent = Intent.parseUri("http://www.google.com", 0);
+ ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
+ .setShortLabel("short label")
+ .setLongLabel("long label")
+ .setIntent(intent)
+ .setIcon(IconCompat.createWithContentUri("content://abc"))
+ .build();
+
+ mShortcutInfoChangeListener.onShortcutAdded(Collections.singletonList(shortcut));
+
+ // Sleep to make sure the asynchronous call finishes. Since the method is mocked this
+ // should be really quick.
+ Thread.sleep(100);
+ verify(mFirebaseUserActions, never()).end(any());
+ }
+
+ @Test
+ @SmallTest
public void onShortcutUpdated_withCapabilityBinding_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+ when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
Intent intent = Intent.parseUri("http://www.google.com", 0);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -166,6 +224,7 @@
@SmallTest
public void onShortcutUpdated_withCapabilityBindingNoParams_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+ when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
Intent intent = Intent.parseUri("http://www.google.com", 0);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "publicIntent")
@@ -196,6 +255,7 @@
@SmallTest
public void onShortcutUpdated_privateIntent_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+ when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
String privateIntentUri = "#Intent;component=androidx.core.google.shortcuts.test/androidx"
+ ".core.google.shortcuts.TrampolineActivity;end";
@@ -224,6 +284,7 @@
@SmallTest
public void onShortcutAdded_savesToAppIndex() throws Exception {
ArgumentCaptor<Indexable> indexableCaptor = ArgumentCaptor.forClass(Indexable.class);
+ when(mFirebaseAppIndex.update(any())).thenReturn(Tasks.forResult(null));
Intent intent = Intent.parseUri("http://www.google.com", 0);
ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(mContext, "intent")
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 506441c..7636cd7 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
@@ -24,6 +24,7 @@
import static androidx.core.google.shortcuts.ShortcutUtils.SHORTCUT_URL_KEY;
import android.content.Context;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -50,6 +51,8 @@
*/
@RestrictTo(LIBRARY_GROUP)
public class ShortcutInfoChangeListenerImpl extends ShortcutInfoChangeListener {
+ private static final String TAG = "ShortcutInfoChangeListe";
+
private final Context mContext;
private final FirebaseAppIndex mFirebaseAppIndex;
private final FirebaseUserActions mFirebaseUserActions;
@@ -81,7 +84,7 @@
*/
@Override
public void onShortcutAdded(@NonNull List<ShortcutInfoCompat> shortcuts) {
- mFirebaseAppIndex.update(buildIndexables(shortcuts));
+ updateAndReportUsage(shortcuts);
}
/**
@@ -91,7 +94,7 @@
*/
@Override
public void onShortcutUpdated(@NonNull List<ShortcutInfoCompat> shortcuts) {
- mFirebaseAppIndex.update(buildIndexables(shortcuts));
+ updateAndReportUsage(shortcuts);
}
/**
@@ -132,6 +135,25 @@
mFirebaseAppIndex.removeAll();
}
+ private void updateAndReportUsage(@NonNull List<ShortcutInfoCompat> shortcuts) {
+ mFirebaseAppIndex.update(buildIndexables(shortcuts))
+ .continueWithTask((task) -> {
+ if (task.isSuccessful()) {
+ reportUsages(shortcuts);
+ } else {
+ Log.e(TAG, "failed to update shortcuts to firebase", task.getException());
+ }
+ return null;
+ });
+ }
+
+ private void reportUsages(@NonNull List<ShortcutInfoCompat> shortcuts) {
+ for (ShortcutInfoCompat shortcut : shortcuts) {
+ String url = ShortcutUtils.getIndexableUrl(mContext, shortcut.getId());
+ mFirebaseUserActions.end(buildAction(url));
+ }
+ }
+
@NonNull
private Action buildAction(@NonNull String url) {
// The reported action isn't uploaded to the server.
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e4767be..4fd4210 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -955,6 +955,7 @@
ctor protected ContextCompat();
method public static int checkSelfPermission(android.content.Context, String);
method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+ method public static String? getAttributionTag(android.content.Context);
method public static java.io.File! getCodeCacheDir(android.content.Context);
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1619,6 +1620,10 @@
method @Deprecated public static void setCounter(String, int);
}
+ @RequiresApi(17) public class UserHandleCompat {
+ method public static android.os.UserHandle getUserHandleForUid(int);
+ }
+
public class UserManagerCompat {
method public static boolean isUserUnlocked(android.content.Context);
}
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index 862c495..f0121f6 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -955,6 +955,7 @@
ctor protected ContextCompat();
method public static int checkSelfPermission(android.content.Context, String);
method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+ method public static String? getAttributionTag(android.content.Context);
method public static java.io.File! getCodeCacheDir(android.content.Context);
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1617,6 +1618,10 @@
method @Deprecated public static void setCounter(String, int);
}
+ @RequiresApi(17) public class UserHandleCompat {
+ method public static android.os.UserHandle getUserHandleForUid(int);
+ }
+
public class UserManagerCompat {
method public static boolean isUserUnlocked(android.content.Context);
}
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index a1037c06..b83a7d3 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1058,6 +1058,7 @@
ctor protected ContextCompat();
method public static int checkSelfPermission(android.content.Context, String);
method public static android.content.Context? createDeviceProtectedStorageContext(android.content.Context);
+ method public static String? getAttributionTag(android.content.Context);
method public static java.io.File! getCodeCacheDir(android.content.Context);
method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
@@ -1938,6 +1939,10 @@
method @Deprecated public static void setCounter(String, int);
}
+ @RequiresApi(17) public class UserHandleCompat {
+ method public static android.os.UserHandle getUserHandleForUid(int);
+ }
+
public class UserManagerCompat {
method public static boolean isUserUnlocked(android.content.Context);
}
diff --git a/core/core/proguard-rules.pro b/core/core/proguard-rules.pro
index 4efb0d5..47a95b5 100644
--- a/core/core/proguard-rules.pro
+++ b/core/core/proguard-rules.pro
@@ -8,3 +8,6 @@
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.app.NotificationCompat$*$Api*Impl {
<methods>;
}
+-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
+ <methods>;
+}
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index d728332..60e2c56 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -352,6 +352,17 @@
}
@Test
+ public void testGetAttributionTag() throws Throwable {
+ assertEquals("Unattributed context", null, ContextCompat.getAttributionTag(mContext));
+
+ if (Build.VERSION.SDK_INT >= 30) {
+ // The following test is only expected to pass on v30+ devices
+ Context attributed = mContext.createAttributionContext("test");
+ assertEquals("Attributed context", "test", ContextCompat.getAttributionTag(attributed));
+ }
+ }
+
+ @Test
public void testGetColor() throws Throwable {
assertEquals("Unthemed color load", 0xFFFF8090,
ContextCompat.getColor(mContext, R.color.text_color));
diff --git a/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java b/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java
new file mode 100644
index 0000000..0e38ec4a
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/os/UserHandleCompatTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.core.os;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Process;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@SdkSuppress(minSdkVersion = 17)
+@SmallTest
+public class UserHandleCompatTest {
+
+ @Test
+ public void testGetUserHandleForUid() {
+ assertEquals(Process.myUserHandle(), UserHandleCompat.getUserHandleForUid(Process.myUid()));
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index 6b0af66..0497817f3 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -138,11 +138,11 @@
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.os.EnvironmentCompat;
+import androidx.core.os.ExecutorCompat;
import java.io.File;
import java.util.HashMap;
import java.util.concurrent.Executor;
-import java.util.concurrent.RejectedExecutionException;
/**
* Helper for accessing features in {@link android.content.Context}.
@@ -163,6 +163,22 @@
}
/**
+ * <p>Attribution can be used in complex apps to logically separate parts of the app. E.g. a
+ * blogging app might also have a instant messaging app built in. In this case two separate tags
+ * can for used each sub-feature.
+ *
+ * @return the attribution tag this context is for or {@code null} if this is the default.
+ */
+ @Nullable
+ public static String getAttributionTag(@NonNull Context context) {
+ if (Build.VERSION.SDK_INT >= 30) {
+ return context.getAttributionTag();
+ }
+
+ return null;
+ }
+
+ /**
* Start a set of activities as a synthesized task stack, if able.
*
* <p>In API level 11 (Android 3.0/Honeycomb) the recommended conventions for
@@ -663,22 +679,7 @@
if (Build.VERSION.SDK_INT >= 28) {
return context.getMainExecutor();
}
- return new MainHandlerExecutor(new Handler(context.getMainLooper()));
- }
-
- private static class MainHandlerExecutor implements Executor {
- private final Handler mHandler;
-
- MainHandlerExecutor(@NonNull Handler handler) {
- mHandler = handler;
- }
-
- @Override
- public void execute(Runnable command) {
- if (!mHandler.post(command)) {
- throw new RejectedExecutionException(mHandler + " is shutting down");
- }
- }
+ return ExecutorCompat.create(new Handler(context.getMainLooper()));
}
/**
diff --git a/core/core/src/main/java/androidx/core/os/UserHandleCompat.java b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
new file mode 100644
index 0000000..761e388
--- /dev/null
+++ b/core/core/src/main/java/androidx/core/os/UserHandleCompat.java
@@ -0,0 +1,103 @@
+/*
+ * 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.core.os;
+
+import android.os.Build;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * Helper for accessing features in {@link android.os.UserHandle} in a backwards compatible
+ * fashion.
+ */
+@RequiresApi(17)
+public class UserHandleCompat {
+
+ @Nullable
+ private static Method sGetUserIdMethod;
+ @Nullable
+ private static Constructor<UserHandle> sUserHandleConstructor;
+
+ private UserHandleCompat() {
+ }
+
+ /**
+ * Returns the user handle for a given uid.
+ */
+ @NonNull
+ public static UserHandle getUserHandleForUid(int uid) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return Api24Impl.getUserHandleForUid(uid);
+ } else {
+ try {
+ Integer userId = (Integer) getGetUserIdMethod().invoke(null, uid);
+ return getUserHandleConstructor().newInstance(userId);
+ } catch (NoSuchMethodException e) {
+ Error error = new NoSuchMethodError();
+ error.initCause(e);
+ throw error;
+ } catch (IllegalAccessException e) {
+ Error error = new IllegalAccessError();
+ error.initCause(e);
+ throw error;
+ } catch (InstantiationException e) {
+ Error error = new InstantiationError();
+ error.initCause(e);
+ throw error;
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ @RequiresApi(24)
+ private static class Api24Impl {
+
+ private Api24Impl() {
+ }
+
+ @NonNull
+ static UserHandle getUserHandleForUid(int uid) {
+ return UserHandle.getUserHandleForUid(uid);
+ }
+ }
+
+ private static Method getGetUserIdMethod() throws NoSuchMethodException {
+ if (sGetUserIdMethod == null) {
+ sGetUserIdMethod = UserHandle.class.getDeclaredMethod("getUserId", Integer.class);
+ sGetUserIdMethod.setAccessible(true);
+ }
+
+ return sGetUserIdMethod;
+ }
+
+ private static Constructor<UserHandle> getUserHandleConstructor() throws NoSuchMethodException {
+ if (sUserHandleConstructor == null) {
+ sUserHandleConstructor = UserHandle.class.getDeclaredConstructor(Integer.class);
+ sUserHandleConstructor.setAccessible(true);
+ }
+
+ return sUserHandleConstructor;
+ }
+}
diff --git a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
index 81e0c71..68a1b37 100644
--- a/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
+++ b/core/core/src/main/java/androidx/core/widget/NestedScrollView.java
@@ -48,6 +48,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
+import androidx.core.R;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.InputDeviceCompat;
import androidx.core.view.NestedScrollingChild3;
@@ -191,7 +192,7 @@
}
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
- this(context, attrs, 0);
+ this(context, attrs, R.attr.nestedScrollViewStyle);
}
public NestedScrollView(@NonNull Context context, @Nullable AttributeSet attrs,
diff --git a/core/core/src/main/res/values/attrs.xml b/core/core/src/main/res/values/attrs.xml
index ce64ab9..b77c646 100644
--- a/core/core/src/main/res/values/attrs.xml
+++ b/core/core/src/main/res/values/attrs.xml
@@ -15,6 +15,9 @@
~ limitations under the License.
-->
<resources>
+ <!-- Default style for NestedScrollView. Should be set in your theme. -->
+ <attr name="nestedScrollViewStyle" format="reference" />
+
<!-- Attributes that are read when parsing a <fontfamily> tag. -->
<declare-styleable name="FontFamily">
<!-- The authority of the Font Provider to be used for the request. -->
diff --git a/development/auto-version-updater/README.md b/development/auto-version-updater/README.md
new file mode 100644
index 0000000..f4a7d8d
--- /dev/null
+++ b/development/auto-version-updater/README.md
@@ -0,0 +1,26 @@
+# Auto Version Updater
+
+This script will update versions in LibraryVersions.kt based on Jetpad.
+
+It automatically runs `updateApi` and `repo upload . --cbr --label Presubmit-Ready+1`.
+
+### Using the script
+
+```bash
+./update_versions_for_release.py 1234
+```
+
+Where 1234 is the Jetpad release id.
+
+To use it without creating a commit and uploading a comment, run:
+
+```bash
+./update_versions_for_release.py 1234 --no-commit
+```
+
+### Testing the script
+
+Script test suite
+```bash
+./test_update_versions_for_release.py
+```
\ No newline at end of file
diff --git a/development/auto-version-updater/test_update_versions_for_release.py b/development/auto-version-updater/test_update_versions_for_release.py
new file mode 100755
index 0000000..aa3648a
--- /dev/null
+++ b/development/auto-version-updater/test_update_versions_for_release.py
@@ -0,0 +1,91 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import unittest
+import os
+from update_versions_for_release import *
+
+class TestVersionUpdates(unittest.TestCase):
+
+ def test_increment_version(self):
+ new_version = increment_version("1.0.0-alpha01")
+ self.assertEqual("1.0.0-alpha02", new_version)
+
+ new_version = increment_version("1.1.0-alpha01")
+ self.assertEqual("1.1.0-alpha02", new_version)
+
+ new_version = increment_version("1.0.0-alpha19")
+ self.assertEqual("1.0.0-alpha20", new_version)
+
+ new_version = increment_version("1.0.0-rc01")
+ self.assertEqual("1.1.0-alpha01", new_version)
+
+ new_version = increment_version("1.3.0-beta02")
+ self.assertEqual("1.3.0-beta03", new_version)
+
+ new_version = increment_version("1.0.1")
+ self.assertEqual("1.1.0-alpha01", new_version)
+
+ def test_get_higher_version(self):
+ higher_version = get_higher_version("1.0.0-alpha01", "1.0.0-alpha02")
+ self.assertEqual("1.0.0-alpha02", higher_version)
+
+ higher_version = get_higher_version("1.0.0-alpha02", "1.0.0-alpha01")
+ self.assertEqual("1.0.0-alpha02", higher_version)
+
+ higher_version = get_higher_version("1.0.0-alpha02", "1.0.0-alpha02")
+ self.assertEqual("1.0.0-alpha02", higher_version)
+
+ higher_version = get_higher_version("1.1.0-alpha01", "1.0.0-alpha02")
+ self.assertEqual("1.1.0-alpha01", higher_version)
+
+ higher_version = get_higher_version("1.0.0-rc05", "1.2.0-beta02")
+ self.assertEqual("1.2.0-beta02", higher_version)
+
+ higher_version = get_higher_version("1.3.0-beta01", "1.5.0-beta01")
+ self.assertEqual("1.5.0-beta01", higher_version)
+
+ higher_version = get_higher_version("3.0.0-alpha01", "1.0.0-alpha02")
+ self.assertEqual("3.0.0-alpha01", higher_version)
+
+ higher_version = get_higher_version("1.0.0-beta01", "1.0.0-rc01")
+ self.assertEqual("1.0.0-rc01", higher_version)
+
+ higher_version = get_higher_version("1.4.0-beta01", "1.0.2")
+ self.assertEqual("1.4.0-beta01", higher_version)
+
+ higher_version = get_higher_version("1.4.0-beta01", "1.4.2")
+ self.assertEqual("1.4.2", higher_version)
+
+ higher_version = get_higher_version("1.4.0", "1.4.2")
+ self.assertEqual("1.4.2", higher_version)
+
+ def test_should_update_version_in_library_versions_kt(self):
+ generic_line = " val CONTENTPAGER = Version(\"1.1.0-alpha01\")"
+ compose_line = " val COMPOSE = Version(System.getenv(\"COMPOSE_CUSTOM_VERSION\") ?: \"1.0.0-beta04\")"
+ self.assertTrue(should_update_version_in_library_versions_kt(generic_line, "1.1.0-alpha02"))
+ self.assertTrue(should_update_version_in_library_versions_kt(generic_line, "1.3.0-alpha01"))
+ self.assertFalse(should_update_version_in_library_versions_kt(generic_line, "1.0.0-alpha01"))
+
+ self.assertTrue(should_update_version_in_library_versions_kt(compose_line, "1.1.0-alpha02"))
+ self.assertTrue(should_update_version_in_library_versions_kt(compose_line, "1.3.0-alpha01"))
+ self.assertFalse(should_update_version_in_library_versions_kt(compose_line, "1.0.0-alpha01"))
+
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
new file mode 100755
index 0000000..9031d71
--- /dev/null
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -0,0 +1,323 @@
+#!/usr/bin/python3
+#
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+import sys
+import os
+import argparse
+from datetime import date
+import subprocess
+from shutil import rmtree
+from shutil import copyfile
+from distutils.dir_util import copy_tree
+from distutils.dir_util import DistutilsFileError
+
+# Import the JetpadClient from the parent directory
+sys.path.append("..")
+from JetpadClient import *
+
+# cd into directory of script
+os.chdir(os.path.dirname(os.path.abspath(__file__)))
+
+FRAMEWORKS_SUPPORT_FP = os.path.abspath(os.path.join(os.getcwd(), '..', '..'))
+LIBRARY_VERSIONS_REL = './buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt'
+LIBRARY_VERSIONS_FP = os.path.join(FRAMEWORKS_SUPPORT_FP, LIBRARY_VERSIONS_REL)
+
+# Set up input arguments
+parser = argparse.ArgumentParser(
+ description=("""Updates androidx library versions for a given release date.
+ This script takes in a the release date as millisecond since the epoch,
+ which is the unique id for the release in Jetpad. It queries the
+ Jetpad db, then creates an output json file with the release information.
+ Finally, updates LibraryVersions.kt and runs updateApi."""))
+parser.add_argument(
+ 'date',
+ help='Milliseconds since epoch')
+parser.add_argument(
+ '--no-commit', action="store_true",
+ help='If specified, this script will not commit the changes')
+
+def print_e(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+
+def ask_yes_or_no(question):
+ """Primpts a yes or no question to the user.
+
+ Args:
+ question: the question to asked.
+
+ Returns:
+ boolean representing yes or no.
+ """
+ while(True):
+ reply = str(input(question+' (y/n): ')).lower().strip()
+ if reply:
+ if reply[0] == 'y': return True
+ if reply[0] == 'n': return False
+ print("Please respond with y/n")
+
+
+def run_update_api():
+ """Runs updateApi from the frameworks/support root.
+ """
+ gradle_cmd = "cd " + FRAMEWORKS_SUPPORT_FP + " && ./gradlew updateApi"
+ try:
+ subprocess.check_output(gradle_cmd, stderr=subprocess.STDOUT, shell=True)
+ except subprocess.CalledProcessError:
+ print_e('FAIL: Unable run updateApi with command: %s' % gradle_cmd)
+ return None
+ return True
+
+
+def convert_prerelease_type_to_num(prerelease_type):
+ """" Convert a prerelease suffix type to its numeric equivalent.
+
+ Args:
+ prerelease_type: the androidx SemVer version prerelease suffix.
+
+ Returns:
+ An int representing that suffix.
+ """
+ if prerelease_type == 'alpha':
+ return 0
+ if prerelease_type == 'beta':
+ return 1
+ if prerelease_type == 'rc':
+ return 2
+ # Stable defaults to 9
+ return 9
+
+
+def parse_version(version):
+ """Converts a SemVer androidx version string into a list of ints.
+
+ Accepts a SemVer androidx version string, such as "1.2.0-alpha02" and
+ returns a list of integers representing the version in the following format:
+ [<major>,<minor>,<bugfix>,<prerelease-suffix>,<prerelease-suffix-revision>]
+ For example 1.2.0-alpha02" returns [1,2,0,0,2]
+
+ Args:
+ version: the androidx version string.
+
+ Returns:
+ a list of integers representing the version.
+ """
+ version_elements = version.split('-')[0].split('.')
+ version_list = []
+ for element in version_elements:
+ version_list.append(int(element))
+ # Check if version contains prerelease suffix
+ version_prerelease_suffix = version.split('-')[-1]
+ # Account for suffixes with only 1 suffix number, i.e. "1.1.0-alphaX"
+ version_prerelease_suffix_rev = version_prerelease_suffix[-2:]
+ version_prerelease_suffix_type = version_prerelease_suffix[:-2]
+ if not version_prerelease_suffix_rev.isnumeric():
+ version_prerelease_suffix_rev = version_prerelease_suffix[-1:]
+ version_prerelease_suffix_type = version_prerelease_suffix[:-1]
+ version_list.append(convert_prerelease_type_to_num(version_prerelease_suffix_type))
+ if version.find("-") == -1:
+ # Version contains no prerelease suffix
+ version_list.append(99)
+ else:
+ version_list.append(int(version_prerelease_suffix_rev))
+ return version_list
+
+
+def get_higher_version(version_a, version_b):
+ """Given two androidx SemVer versions, returns the greater one.
+
+ Args:
+ version_a: first version to be compared.
+ version_b: second version to be compared.
+
+ Returns:
+ The greater of version_a and version_b.
+ """
+ version_a_list = parse_version(version_a)
+ version_b_list = parse_version(version_b)
+ for i in range(len(version_a_list)):
+ if version_a_list[i] > version_b_list[i]:
+ return version_a
+ if version_a_list[i] < version_b_list[i]:
+ return version_b
+ return version_a
+
+
+def should_update_version_in_library_versions_kt(line, new_version):
+ """Returns true if the new_version is greater than the version in line.
+
+ Args:
+ line: a line in LibraryVersions.kt file.
+ new_version: the version to check again.
+
+ Returns:
+ True if should update version, false otherwise.
+ """
+ if 'Version(' not in line:
+ return False
+ # Find the first piece with a numeric first character.
+ split_current_line = line.split('"')
+ i = 1
+ while (not split_current_line[i][0].isnumeric() and
+ i < len(split_current_line)):
+ i += 1
+ if i == len(split_current_line):
+ return False
+ version = split_current_line[i]
+ return new_version == get_higher_version(version, new_version)
+
+
+def increment_version(version):
+ """Increments an androidx SemVer version.
+
+ If the version is alpha or beta, the suffix is simply incremented.
+ Otherwise, it chooses the next minor version.
+
+ Args:
+ version: the version to be incremented.
+
+ Returns:
+ The incremented version.
+ """
+ if "alpha" in version or "beta" in version:
+ version_prerelease_suffix = version[-2:]
+ new_version_prerelease_suffix = int(version_prerelease_suffix) + 1
+ new_version = version[:-2] + "%02d" % (new_version_prerelease_suffix,)
+ else:
+ version_minor = version.split(".")[1]
+ new_version_minor = str(int(version_minor) + 1)
+ new_version = version.split(".")[0] + "." + new_version_minor + ".0-alpha01"
+ return new_version
+
+
+def update_versions_in_library_versions_kt(group_id, artifact_id, old_version):
+ """Updates the versions in the LibrarVersions.kt file.
+
+ This will take the old_version and increment it to find the appropriate
+ new version.
+
+ Args:
+ group_id: group_id of the existing library
+ artifact_id: artifact_id of the existing library
+ old_version: old version of the existing library
+
+ Returns:
+ True if the version was updated, false otherwise.
+ """
+ group_id_variable_name = group_id.replace("androidx.","").replace(".","_").upper()
+ artifact_id_variable_name = artifact_id.replace("androidx.","").replace("-","_").upper()
+ new_version = increment_version(old_version)
+ # Special case Compose because it uses the same version variable.
+ if group_id_variable_name.startswith("COMPOSE"):
+ group_id_variable_name = "COMPOSE"
+
+ # Open file for reading and get all lines
+ with open(LIBRARY_VERSIONS_FP, 'r') as f:
+ library_versions_lines = f.readlines()
+ num_lines = len(library_versions_lines)
+ updated_version = False
+
+ # First check any artifact ids with unique versions.
+ for i in range(num_lines):
+ cur_line = library_versions_lines[i]
+ # Skip any line that doesn't declare a version
+ if 'Version(' not in cur_line: continue
+ version_variable_name = cur_line.split('val ')[1].split(' =')[0]
+ if artifact_id_variable_name == version_variable_name:
+ if not should_update_version_in_library_versions_kt(cur_line, new_version):
+ break
+ # Found the correct variable to modify
+ if version_variable_name == "COMPOSE":
+ new_version_line = (" val COMPOSE = Version("
+ "System.getenv(\"COMPOSE_CUSTOM_VERSION\") "
+ "?: \"" + new_version + "\")\n")
+ else:
+ new_version_line = " val " + version_variable_name + \
+ " = Version(\"" + new_version + "\")\n"
+ library_versions_lines[i] = new_version_line
+ updated_version = True
+ break
+
+ if not updated_version:
+ # Then check any group ids.
+ for i in range(num_lines):
+ cur_line = library_versions_lines[i]
+ # Skip any line that doesn't declare a version
+ if 'Version(' not in cur_line: continue
+ version_variable_name = cur_line.split('val ')[1].split(' =')[0]
+ if group_id_variable_name == version_variable_name:
+ if not should_update_version_in_library_versions_kt(cur_line, new_version):
+ break
+ # Found the correct variable to modify
+ if version_variable_name == "COMPOSE":
+ new_version_line = (" val COMPOSE = Version("
+ "System.getenv(\"COMPOSE_CUSTOM_VERSION\") "
+ "?: \"" + new_version + "\")\n")
+ else:
+ new_version_line = " val " + version_variable_name + \
+ " = Version(\"" + new_version + "\")\n"
+ library_versions_lines[i] = new_version_line
+ updated_version = True
+ break
+
+ # Open file for writing and update all lines
+ with open(LIBRARY_VERSIONS_FP, 'w') as f:
+ f.writelines(library_versions_lines)
+ return updated_version
+
+
+def commit_updates(release_date):
+ subprocess.check_call(['git', 'add', FRAMEWORKS_SUPPORT_FP])
+ # ensure that we've actually made a change:
+ staged_changes = subprocess.check_output('git diff --cached', stderr=subprocess.STDOUT, shell=True)
+ if not staged_changes:
+ return
+ msg = "Update versions for release id %s\n\nThis commit was generated from the command:\n%s\n\n%s" % (release_date, " ".join(sys.argv), 'Test: ./gradlew checkApi')
+ subprocess.check_call(['git', 'commit', '-m', msg])
+ subprocess.check_call(['yes', '|', 'repo', 'upload', '.', '--cbr', '--label', 'Presubmit-Ready+1'])
+
+def main(args):
+ # Parse arguments and check for existence of build ID or file
+ args = parser.parse_args()
+ if not args.date:
+ parser.error("You must specify a release date in Milliseconds since epoch")
+ sys.exit(1)
+ release_json_object = getJetpadRelease(args.date, False)
+ non_updated_libraries = []
+ for group_id in release_json_object["modules"]:
+ for artifact in release_json_object["modules"][group_id]:
+ updated = update_versions_in_library_versions_kt(group_id,
+ artifact["artifactId"], artifact["version"])
+ if not updated:
+ non_updated_libraries.append("%s:%s:%s" % (group_id,
+ artifact["artifactId"],
+ artifact["version"]))
+ if non_updated_libraries:
+ print("The following libraries were not updated:")
+ for library in non_updated_libraries:
+ print("\t", library)
+ print("Updated library versions. \nRunning updateApi for the new "
+ "versions, this may take a minute...", end='')
+ if run_update_api():
+ print("done.")
+ else:
+ print_e("failed. Please investigate manually.")
+ if not args.no_commit:
+ commit_updates(args.date)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 4c612db..4e3dea0 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -215,6 +215,7 @@
DIST_DIR=\$DIST_DIR
CHECKOUT=\$CHECKOUT
GRADLE_USER_HOME=\$GRADLE_USER_HOME
+Starting a Gradle Daemon \(subsequent builds will be faster\)
Downloading file\:\$SUPPORT\/gradle\/wrapper\/\.\.\/\.\.\/\.\.\/\.\.\/tools\/external\/gradle\/gradle\-[0-9]+\.[0-9]+\.[0-9]+\-bin\.zip
\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%\.\.\.\.\.\.\.\.\.\.[0-9]+%
Welcome to Gradle [0-9]+\.[0-9]+\.[0-9]+\!
@@ -328,8 +329,8 @@
# > Task :support-preference-demos:compileDebugJavaWithJavac
# > Task :startup:integration-tests:first-library:processDebugManifest
\$SUPPORT/startup/integration\-tests/first\-library/src/main/AndroidManifest\.xml:[0-9]+:[0-9]+\-[0-9]+:[0-9]+ Warning:
-meta\-data\#androidx\.work\.impl\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
-provider\#androidx\.work\.impl\.WorkManagerInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
+meta\-data\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml\:[0-9]+ to remove other declarations but no other declaration present
+provider\#androidx\.work\.WorkManagerInitializer was tagged at AndroidManifest\.xml:[0-9]+ to remove other declarations but no other declaration present
# > Task :camera:integration-tests:camera-testapp-extensions:compileDebugJavaWithJavac
# > Task :camera:integration-tests:camera-testapp-core:compileDebugJavaWithJavac
# > Task :room:integration-tests:room-testapp:processDebugMainManifest
@@ -553,6 +554,7 @@
java\.lang\.Object androidx\.compose\.foundation\.gestures\.DragGestureDetectorKt\.awaitHorizontalTouchSlopOrCancellation\-jO[0-9]+t[0-9]+\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, long, kotlin\.jvm\.functions\.Function[0-9]+, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.compose\.foundation\.gestures\.TransformGestureDetectorKt\$detectTransformGestures\$[0-9]+\$[0-9]+\.invokeSuspend\(java\.lang\.Object\)
Type information in locals\-table is inconsistent\. Cannot constrain type: INT for value: v[0-9]+\(index\$iv\$iv\) by constraint FLOAT\.
+java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.translatePointerEventsToChannel\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlinx\.coroutines\.CoroutineScope, kotlinx\.coroutines\.channels\.SendChannel, androidx\.compose\.runtime\.State, androidx\.compose\.runtime\.MutableState, kotlin\.coroutines\.Continuation\)
java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.AwaitPointerEventScope, kotlin\.coroutines\.Continuation\)
# > Task :preference:preference:compileDebugAndroidTestKotlin
w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
@@ -608,4 +610,4 @@
# > Task :compose:animation:animation-core:animation-core-benchmark:processReleaseAndroidTestManifest
\$OUT_DIR\/androidx\/compose\/animation\/animation\-core\/animation\-core\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
# > Task :compose:ui:ui-graphics:ui-graphics-benchmark:processReleaseAndroidTestManifest
-\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
+\$OUT_DIR\/androidx\/compose\/ui\/ui\-graphics\/ui\-graphics\-benchmark\/build\/intermediates\/tmp\/manifest\/androidTest\/release\/manifestMerger[0-9]+\.xml\:[0-9]+\:[0-9]+\-[0-9]+\:[0-9]+ Warning\:
\ No newline at end of file
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index 9c9513a..63b7fea 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -446,7 +446,11 @@
}
}
val variantNames = listOf(
- "runtimeElements", "releaseRuntimePublication", "metadata-api", "runtime"
+ "runtimeElements",
+ "releaseRuntimePublication",
+ "metadata-api",
+ "metadataApiElements-published",
+ "runtime"
)
variantNames.forEach { name ->
ctx.details.maybeAddVariant("allFilesWithDependencies${name.capitalize()}", name) {
@@ -464,6 +468,7 @@
addFile("${id.name}-${id.version}.jar")
addFile("${id.name}-${id.version}.aar")
addFile("${id.name}-${id.version}-sources.jar")
+ addFile("${id.name}-${id.version}.klib")
}
}
}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 9111882..78a2806 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -37,6 +37,7 @@
docs(project(":camera:camera-extensions"))
stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
docs(project(":camera:camera-lifecycle"))
+ docs(project(":camera:camera-video"))
docs(project(":camera:camera-view"))
docs(project(":car:app:app"))
docs(project(":car:app:app-activity"))
@@ -56,12 +57,15 @@
docs(project(":compose:material:material-icons-core"))
samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
docs(project(":compose:material:material-icons-extended"))
+ docs(project(":compose:material:material-ripple"))
samples(project(":compose:material:material:material-samples"))
docs(project(":compose:runtime:runtime"))
docs(project(":compose:runtime:runtime-livedata"))
samples(project(":compose:runtime:runtime-livedata:runtime-livedata-samples"))
docs(project(":compose:runtime:runtime-rxjava2"))
samples(project(":compose:runtime:runtime-rxjava2:runtime-rxjava2-samples"))
+ docs(project(":compose:runtime:runtime-rxjava3"))
+ samples(project(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples"))
docs(project(":compose:runtime:runtime-saveable"))
samples(project(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
samples(project(":compose:runtime:runtime:runtime-samples"))
@@ -76,6 +80,7 @@
docs(project(":compose:ui:ui-text"))
samples(project(":compose:ui:ui-text:ui-text-samples"))
docs(project(":compose:ui:ui-tooling"))
+ docs(project(":compose:ui:ui-tooling-data"))
docs(project(":compose:ui:ui-unit"))
samples(project(":compose:ui:ui-unit:ui-unit-samples"))
docs(project(":compose:ui:ui-util"))
@@ -100,8 +105,11 @@
docs(project(":datastore:datastore-preferences"))
docs(project(":datastore:datastore-preferences-core"))
docs(project(":datastore:datastore-preferences-core:datastore-preferences-proto"))
+ docs(project(":datastore:datastore-preferences-rxjava2"))
+ docs(project(":datastore:datastore-preferences-rxjava3"))
docs(project(":datastore:datastore-proto"))
docs(project(":datastore:datastore-rxjava2"))
+ docs(project(":datastore:datastore-rxjava3"))
docs(project(":documentfile:documentfile"))
docs(project(":drawerlayout:drawerlayout"))
docs(project(":dynamicanimation:dynamicanimation"))
@@ -142,6 +150,7 @@
docs(project(":lifecycle:lifecycle-service"))
docs(project(":lifecycle:lifecycle-viewmodel"))
docs(project(":lifecycle:lifecycle-viewmodel-compose"))
+ samples(project(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
docs(project(":lifecycle:lifecycle-viewmodel-ktx"))
docs(project(":lifecycle:lifecycle-viewmodel-savedstate"))
docs(project(":loader:loader"))
@@ -216,6 +225,7 @@
docs(project(":startup:startup-runtime"))
docs(project(":legacy-support-core-utils"))
docs(project(":swiperefreshlayout:swiperefreshlayout"))
+ docs(project(":text:text"))
docs(project(":textclassifier:textclassifier"))
docs(project(":tracing:tracing"))
docs(project(":tracing:tracing-ktx"))
@@ -232,6 +242,7 @@
stubs(fileTree(dir: "../wear/wear_stubs/", include: ["com.google.android.wearable-stubs.jar"]))
docs(project(":wear:wear-complications-data"))
docs(project(":wear:wear-complications-provider"))
+ samples(project(":wear:wear-complications-provider-samples"))
docs(project(":wear:wear-input"))
docs(project(":wear:wear-input-testing"))
docs(project(":wear:wear-ongoing"))
@@ -247,7 +258,9 @@
docs(project(":wear:wear-watchface-data"))
docs(project(":wear:wear-watchface-editor"))
docs(project(":wear:wear-watchface-editor-guava"))
+ docs(project(":wear:wear-watchface-editor-samples"))
docs(project(":wear:wear-watchface-guava"))
+ samples(project(":wear:wear-watchface-samples"))
docs(project(":wear:wear-watchface-style"))
docs(project(":webkit:webkit"))
docs(project(":window:window"))
diff --git a/emoji2/emoji2-benchmark/build.gradle b/emoji2/emoji2-benchmark/build.gradle
index 643376d..17f0792 100644
--- a/emoji2/emoji2-benchmark/build.gradle
+++ b/emoji2/emoji2-benchmark/build.gradle
@@ -13,11 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import static androidx.build.dependencies.DependenciesKt.*
+
import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
import androidx.build.Publish
+import static androidx.build.dependencies.DependenciesKt.*
+
plugins {
id("AndroidXPlugin")
id("com.android.library")
diff --git a/emoji2/emoji2-bundled/build.gradle b/emoji2/emoji2-bundled/build.gradle
index 9d49183..b48c953 100644
--- a/emoji2/emoji2-bundled/build.gradle
+++ b/emoji2/emoji2-bundled/build.gradle
@@ -1,7 +1,8 @@
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.Publish
-import androidx.build.RunApiTasks
+
+import static androidx.build.dependencies.DependenciesKt.*
plugins {
id("AndroidXPlugin")
@@ -14,12 +15,34 @@
android {
sourceSets {
- main.assets.srcDirs new File(fontDir, "font").getAbsolutePath()
+ main {
+ assets.srcDirs new File(fontDir, "font").getAbsolutePath()
+ resources {
+ srcDirs += [fontDir.getAbsolutePath()]
+ includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
+ }
+ }
+
+ androidTest {
+ assets {
+ srcDirs = [new File(fontDir, "supported-emojis").getAbsolutePath()]
+ }
+ }
}
+
}
dependencies {
api(project(":emoji2:emoji2"))
+
+ androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+ androidTestImplementation(ANDROIDX_TEST_CORE)
+ androidTestImplementation(ANDROIDX_TEST_RUNNER)
+ androidTestImplementation(ANDROIDX_TEST_RULES)
+ androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
+ androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation project(':internal-testutils-runtime')
}
androidx {
diff --git a/emoji2/emoji2/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
similarity index 94%
rename from emoji2/emoji2/src/androidTest/AndroidManifest.xml
rename to emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
index 0b2cc8b..3b9ba38f 100644
--- a/emoji2/emoji2/src/androidTest/AndroidManifest.xml
+++ b/emoji2/emoji2-bundled/src/androidTest/AndroidManifest.xml
@@ -14,7 +14,7 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="androidx.emoji2.text">
+ package="androidx.emoji2.bundled">
<application>
<activity android:name=".TestActivity"/>
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
index 6e5a680..1e5a600 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/AllEmojisTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/AllEmojisTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
@@ -23,8 +23,11 @@
import android.text.Spanned;
import androidx.core.graphics.PaintCompat;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiMetadata;
+import androidx.emoji2.text.EmojiSpan;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
index 34525b1..fb1be38 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/ConfigTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/ConfigTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -31,9 +31,10 @@
import android.content.Context;
import android.graphics.Color;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
@@ -58,16 +59,19 @@
@Test(expected = NullPointerException.class)
public void testConstructor_throwsExceptionIfMetadataLoaderNull() {
+ //noinspection ConstantConditions
new TestConfigBuilder.TestConfig(null);
}
@Test(expected = NullPointerException.class)
public void testInitCallback_throwsExceptionIfNull() {
+ //noinspection ConstantConditions
new ValidTestConfig().registerInitCallback(null);
}
@Test(expected = NullPointerException.class)
public void testUnregisterInitCallback_throwsExceptionIfNull() {
+ //noinspection ConstantConditions
new ValidTestConfig().unregisterInitCallback(null);
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
index 46d51bc..2315bff 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiCompatTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiCompatTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
@@ -46,10 +46,12 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
@@ -703,8 +705,8 @@
@Test
@SdkSuppress(maxSdkVersion = 18)
public void testLoad_pre19() {
- final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
- .TestEmojiDataLoader());
+ final EmojiCompat.MetadataRepoLoader loader =
+ Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
@@ -720,8 +722,8 @@
@Test
@SdkSuppress(minSdkVersion = 19)
public void testLoad_startsLoading() {
- final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
- .TestEmojiDataLoader());
+ final EmojiCompat.MetadataRepoLoader loader =
+ Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
@@ -738,8 +740,8 @@
@Test
@SdkSuppress(minSdkVersion = 19)
public void testLoad_onceSuccessDoesNotStartLoading() {
- final EmojiCompat.MetadataRepoLoader loader = Mockito.spy(new TestConfigBuilder
- .TestEmojiDataLoader());
+ final EmojiCompat.MetadataRepoLoader loader =
+ Mockito.spy(new TestConfigBuilder.TestEmojiDataLoader());
final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(loader)
.setMetadataLoadStrategy(EmojiCompat.LOAD_STRATEGY_MANUAL);
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
index 9b813d4..8b4de90 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiKeyboardTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiKeyboardTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertThat;
@@ -22,11 +22,12 @@
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
-import androidx.emoji2.test.R;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.test.R;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.Suppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
similarity index 92%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
index 573a8f4..7ece287 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanInstrumentationTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/EmojiSpanInstrumentationTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
@@ -27,10 +27,12 @@
import android.util.TypedValue;
import android.widget.TextView;
-import androidx.emoji2.test.R;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.test.R;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.EmojiSpan;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
similarity index 86%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
index 92afb67..18cff62 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/FontRequestEmojiCompatConfigTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/FontRequestEmojiCompatConfigTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static android.content.res.AssetManager.ACCESS_BUFFER;
@@ -53,6 +53,9 @@
import androidx.core.provider.FontRequest;
import androidx.core.provider.FontsContractCompat.FontFamilyResult;
import androidx.core.provider.FontsContractCompat.FontInfo;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.FontRequestEmojiCompatConfig;
+import androidx.emoji2.text.MetadataRepo;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
@@ -68,7 +71,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
-import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -84,17 +86,19 @@
public void setup() {
mContext = ApplicationProvider.getApplicationContext();
mFontRequest = new FontRequest("authority", "package", "query",
- new ArrayList<List<byte[]>>());
+ new ArrayList<>());
mFontProviderHelper = mock(FontRequestEmojiCompatConfig.FontProviderHelper.class);
}
@Test(expected = NullPointerException.class)
public void testConstructor_withNullContext() {
+ //noinspection ConstantConditions
new FontRequestEmojiCompatConfig(null, mFontRequest);
}
@Test(expected = NullPointerException.class)
public void testConstructor_withNullFontRequest() {
+ //noinspection ConstantConditions
new FontRequestEmojiCompatConfig(mContext, null);
}
@@ -105,10 +109,10 @@
doThrow(exception).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
- mFontProviderHelper);
+ final TestFontRequestEmojiCompatConfig config =
+ new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, times(1)).onFailed(same(exception));
}
@@ -119,15 +123,16 @@
doThrow(new NameNotFoundException()).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper);
+ final TestFontRequestEmojiCompatConfig config =
+ new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(callback, times(1)).onFailed(argumentCaptor.capture());
- assertThat(argumentCaptor.getValue().getMessage(), containsString("provider not found"));
+ assertThat(argumentCaptor.getValue().getMessage(),
+ containsString("provider not found"));
}
@Test
@@ -195,10 +200,10 @@
doReturn(new FontFamilyResult(STATUS_OK, fonts)).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper);
+ final TestFontRequestEmojiCompatConfig config =
+ new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, times(1)).onLoaded(any(MetadataRepo.class));
}
@@ -215,10 +220,11 @@
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(-1, 1));
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config =
+ new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
+ config.setRetryPolicy(retryPolicy);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, times(1)).onFailed(any(Throwable.class));
@@ -237,10 +243,11 @@
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
final WaitingRetryPolicy retryPolicy = spy(new WaitingRetryPolicy(500, 1));
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config =
+ new TestFontRequestEmojiCompatConfig(mContext, mFontRequest, mFontProviderHelper);
+ config.setRetryPolicy(retryPolicy);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, never()).onFailed(any(Throwable.class));
@@ -271,11 +278,11 @@
try {
Handler handler = new Handler(thread.getLooper());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setHandler(handler)
- .setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+ mContext, mFontRequest, mFontProviderHelper);
+ config.setHandler(handler).setRetryPolicy(retryPolicy);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, never()).onFailed(any(Throwable.class));
@@ -328,11 +335,11 @@
try {
Handler handler = new Handler(thread.getLooper());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setHandler(handler)
- .setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+ mContext, mFontRequest, mFontProviderHelper);
+ config.setHandler(handler).setRetryPolicy(retryPolicy);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, never()).onFailed(any(Throwable.class));
@@ -383,14 +390,14 @@
thread.start();
try {
Handler handler = new Handler(thread.getLooper());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setHandler(handler)
- .setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+ mContext, mFontRequest, mFontProviderHelper);
+ config.setHandler(handler).setRetryPolicy(retryPolicy);
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, never()).onFailed(any(Throwable.class));
@@ -439,14 +446,14 @@
thread.start();
try {
Handler handler = new Handler(thread.getLooper());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext,
- mFontRequest, mFontProviderHelper).setHandler(handler)
- .setRetryPolicy(retryPolicy);
+ final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+ mContext, mFontRequest, mFontProviderHelper);
+ config.setRetryPolicy(retryPolicy).setHandler(handler);
ArgumentCaptor<ContentObserver> observerCaptor =
ArgumentCaptor.forClass(ContentObserver.class);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
retryPolicy.await(DEFAULT_TIMEOUT_MILLIS);
verify(callback, never()).onLoaded(any(MetadataRepo.class));
verify(callback, never()).onFailed(any(Throwable.class));
@@ -482,10 +489,10 @@
doReturn(new FontFamilyResult(statusCode, fonts)).when(mFontProviderHelper).fetchFonts(
any(Context.class), any(FontRequest.class));
final WaitingLoaderCallback callback = spy(new WaitingLoaderCallback());
- final EmojiCompat.Config config = new FontRequestEmojiCompatConfig(mContext, mFontRequest,
- mFontProviderHelper);
+ final TestFontRequestEmojiCompatConfig config = new TestFontRequestEmojiCompatConfig(
+ mContext, mFontRequest, mFontProviderHelper);
- config.getMetadataRepoLoader().load(callback);
+ config.loadForTests(callback);
callback.await(DEFAULT_TIMEOUT_MILLIS);
final ArgumentCaptor<Throwable> argumentCaptor = ArgumentCaptor.forClass(Throwable.class);
@@ -582,4 +589,28 @@
return new FontInfo[] { new FontInfo(Uri.parse("file:///some/placeholder/file"),
0 /* ttc index */, 400 /* weight */, false /* italic */, resultCode) };
}
+
+ /**
+ * This class exists since the test suite is in another package and we need to call the
+ * `protected` method `getMetadataRepoLoader` in these tests.
+ */
+ private static class TestFontRequestEmojiCompatConfig extends FontRequestEmojiCompatConfig {
+
+ TestFontRequestEmojiCompatConfig(
+ @NonNull Context context,
+ @NonNull FontRequest request) {
+ super(context, request);
+ }
+
+ TestFontRequestEmojiCompatConfig(
+ @NonNull Context context,
+ @NonNull FontRequest request,
+ @NonNull FontRequestEmojiCompatConfig.FontProviderHelper fontProviderHelper) {
+ super(context, request, fontProviderHelper);
+ }
+
+ public void loadForTests(EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ getMetadataRepoLoader().load(loaderCallback);
+ }
+ }
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
similarity index 96%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
index 5ea5680..5ef6199 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/HardDeleteTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/HardDeleteTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -24,10 +24,11 @@
import android.text.SpannableStringBuilder;
import android.view.KeyEvent;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.KeyboardUtil;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.KeyboardUtil;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
index 118d823..40329c0 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/InitCallbackTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/InitCallbackTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.nullable;
@@ -23,6 +23,7 @@
import static org.mockito.Mockito.verify;
import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
similarity index 97%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
index 9890903..a945feb 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/SoftDeleteTest.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/SoftDeleteTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -26,9 +26,10 @@
import android.text.SpannableStringBuilder;
import android.view.inputmethod.InputConnection;
-import androidx.emoji2.util.Emoji;
-import androidx.emoji2.util.EmojiMatcher;
-import androidx.emoji2.util.TestString;
+import androidx.emoji2.bundled.util.Emoji;
+import androidx.emoji2.bundled.util.EmojiMatcher;
+import androidx.emoji2.bundled.util.TestString;
+import androidx.emoji2.text.EmojiCompat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
similarity index 92%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
index bae3630..82ae1a0 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestActivity.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestActivity.java
@@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import android.app.Activity;
import android.os.Bundle;
-import androidx.emoji2.test.R;
+import androidx.emoji2.bundled.test.R;
public class TestActivity extends Activity {
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
similarity index 95%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
index e44b5a4..e0f9112 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/TestConfigBuilder.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/TestConfigBuilder.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.text;
+package androidx.emoji2.bundled;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
@@ -24,8 +24,9 @@
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.emoji2.text.EmojiCompat;
+import androidx.emoji2.text.MetadataRepo;
import androidx.test.core.app.ApplicationProvider;
-import androidx.text.emoji.flatbuffer.MetadataList;
import java.util.concurrent.CountDownLatch;
@@ -52,7 +53,7 @@
super(new TestEmojiDataLoader());
}
- TestConfig(final EmojiCompat.MetadataRepoLoader metadataLoader) {
+ TestConfig(@NonNull final EmojiCompat.MetadataRepoLoader metadataLoader) {
super(metadataLoader);
}
}
@@ -88,8 +89,7 @@
try {
mLoaderLatch.await();
if (mSuccess) {
- loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class),
- new MetadataList()));
+ loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class)));
} else {
loaderCallback.onFailed(null);
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
index 73117a2..1413eb4 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/Emoji.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/Emoji.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
import androidx.annotation.NonNull;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
similarity index 99%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
index 32d8e03..3025c68 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/EmojiMatcher.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/EmojiMatcher.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
import static org.mockito.ArgumentMatchers.argThat;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
index 48a6d2e..f640452 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/KeyboardUtil.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/KeyboardUtil.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
import android.app.Instrumentation;
import android.text.Selection;
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
similarity index 98%
rename from emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
rename to emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
index 83728da..eaf0213 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/util/TestString.java
+++ b/emoji2/emoji2-bundled/src/androidTest/java/androidx/emoji2/bundled/util/TestString.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.emoji2.util;
+package androidx.emoji2.bundled.util;
import java.util.ArrayList;
import java.util.List;
diff --git a/emoji2/emoji2/src/androidTest/res/layout/activity_default.xml b/emoji2/emoji2-bundled/src/androidTest/res/layout/activity_default.xml
similarity index 100%
rename from emoji2/emoji2/src/androidTest/res/layout/activity_default.xml
rename to emoji2/emoji2-bundled/src/androidTest/res/layout/activity_default.xml
diff --git a/emoji2/emoji2-bundled/src/main/AndroidManifest.xml b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
index 56ac589..ee983ff 100644
--- a/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
+++ b/emoji2/emoji2-bundled/src/main/AndroidManifest.xml
@@ -13,4 +13,4 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest package="androidx.emoji.bundled"/>
\ No newline at end of file
+<manifest package="androidx.emoji2.bundled"/>
\ No newline at end of file
diff --git a/emoji2/emoji2-views-helper/build.gradle b/emoji2/emoji2-views-helper/build.gradle
index 6137849..4acd93b 100644
--- a/emoji2/emoji2-views-helper/build.gradle
+++ b/emoji2/emoji2-views-helper/build.gradle
@@ -1,4 +1,3 @@
-import androidx.build.BundleInsideHelper
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.Publish
diff --git a/emoji2/emoji2/build.gradle b/emoji2/emoji2/build.gradle
index 2882681..b60150b 100644
--- a/emoji2/emoji2/build.gradle
+++ b/emoji2/emoji2/build.gradle
@@ -2,7 +2,6 @@
import androidx.build.LibraryGroups
import androidx.build.LibraryVersions
import androidx.build.Publish
-import androidx.build.RunApiTasks
import static androidx.build.dependencies.DependenciesKt.*
@@ -12,10 +11,6 @@
id("com.github.johnrengelman.shadow")
}
-ext {
- fontDir = project(':noto-emoji-compat-font').projectDir
-}
-
BundleInsideHelper.forInsideAar(
project,
/* from = */ "com.google.flatbuffers",
@@ -43,17 +38,6 @@
main {
// We use a non-standard manifest path.
manifest.srcFile 'AndroidManifest.xml'
- resources {
- srcDirs += [fontDir.getAbsolutePath()]
- includes += ["LICENSE_UNICODE", "LICENSE_OFL"]
- }
- }
-
- androidTest {
- assets {
- srcDirs = [new File(fontDir, "font").getAbsolutePath(),
- new File(fontDir, "supported-emojis").getAbsolutePath()]
- }
}
}
}
@@ -65,14 +49,4 @@
mavenGroup = LibraryGroups.EMOJI2
inceptionYear = "2017"
description = "Core library to enable emoji compatibility in Kitkat and newer devices to avoid the empty emoji characters."
-
- license {
- name = "SIL Open Font License, Version 1.1"
- url = "http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web"
- }
-
- license {
- name = "Unicode, Inc. License"
- url = "http://www.unicode.org/copyright.html#License"
- }
}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
index 77694aa..ff969db 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/EmojiSpanTest.java
@@ -46,7 +46,7 @@
@Before
public void setup() {
- EmojiCompat.reset(TestConfigBuilder.config());
+ EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig());
}
@Test
@@ -97,14 +97,14 @@
final int bottom = 30;
// verify the case where indicators are disabled
- EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(false));
+ EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig().setEmojiSpanIndicatorEnabled(false));
span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
verify(canvas, times(0)).drawRect(eq(x), eq((float) top), eq(x + spanWidth),
eq((float) bottom), any(Paint.class));
// verify the case where indicators are enabled
- EmojiCompat.reset(TestConfigBuilder.config().setEmojiSpanIndicatorEnabled(true));
+ EmojiCompat.reset(NoFontTestEmojiConfig.emptyConfig().setEmojiSpanIndicatorEnabled(true));
reset(canvas);
span.draw(canvas, "a", 0 /*start*/, 1 /*end*/, x, top, y, bottom, mock(Paint.class));
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
index 3039f17..0a51020 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/MetadataRepoTest.java
@@ -24,7 +24,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
-import androidx.text.emoji.flatbuffer.MetadataList;
import org.junit.Before;
import org.junit.Test;
@@ -39,7 +38,7 @@
@Before
public void clearResourceIndex() {
- mMetadataRepo = MetadataRepo.create(mock(Typeface.class), new MetadataList());
+ mMetadataRepo = MetadataRepo.create(mock(Typeface.class));
}
@Test(expected = NullPointerException.class)
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java
new file mode 100644
index 0000000..812a223
--- /dev/null
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/NoFontTestEmojiConfig.java
@@ -0,0 +1,57 @@
+/*
+ * 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.emoji2.text;
+
+import static org.mockito.Mockito.mock;
+
+import android.graphics.Typeface;
+
+import androidx.annotation.NonNull;
+
+public class NoFontTestEmojiConfig extends EmojiCompat.Config {
+
+ static EmojiCompat.Config emptyConfig() {
+ return new NoFontTestEmojiConfig(new EmptyEmojiDataLoader());
+ }
+
+ static EmojiCompat.Config neverLoadsConfig() {
+ return new NoFontTestEmojiConfig(new NeverCompletesMetadataRepoLoader());
+ }
+
+ static EmojiCompat.Config fromLoader(EmojiCompat.MetadataRepoLoader loader) {
+ return new NoFontTestEmojiConfig(loader);
+ }
+
+ private NoFontTestEmojiConfig(EmojiCompat.MetadataRepoLoader loader) {
+ super(loader);
+ }
+
+ private static class EmptyEmojiDataLoader implements EmojiCompat.MetadataRepoLoader {
+ @Override
+ public void load(@NonNull EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ loaderCallback.onLoaded(MetadataRepo.create(mock(Typeface.class)));
+ }
+ }
+
+ private static class NeverCompletesMetadataRepoLoader
+ implements EmojiCompat.MetadataRepoLoader {
+ @Override
+ public void load(@NonNull final EmojiCompat.MetadataRepoLoaderCallback loaderCallback) {
+ // do nothing, this will be called on the test thread and is a no-op
+ }
+ }
+}
diff --git a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
index 6cc85cd..1a15f44 100644
--- a/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
+++ b/emoji2/emoji2/src/androidTest/java/androidx/emoji2/text/UninitializedStateTest.java
@@ -19,7 +19,6 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.filters.SmallTest;
-import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -29,19 +28,9 @@
@SdkSuppress(minSdkVersion = 19)
public class UninitializedStateTest {
- private TestConfigBuilder.WaitingDataLoader mWaitingDataLoader;
-
@Before
public void setup() {
- mWaitingDataLoader = new TestConfigBuilder.WaitingDataLoader(true);
- final EmojiCompat.Config config = new TestConfigBuilder.TestConfig(mWaitingDataLoader);
- EmojiCompat.reset(config);
- }
-
- @After
- public void after() {
- mWaitingDataLoader.getLoaderLatch().countDown();
- mWaitingDataLoader.getTestLatch().countDown();
+ EmojiCompat.reset(NoFontTestEmojiConfig.neverLoadsConfig());
}
@Test(expected = IllegalStateException.class)
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
index f13c2a9..ac35a2d 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiCompat.java
@@ -509,20 +509,20 @@
}
/**
- * @return whether a background should be drawn for the emoji.
+ * @return whether a background should be drawn for the emoji for debugging
* @hide
*/
- @RestrictTo(LIBRARY)
- boolean isEmojiSpanIndicatorEnabled() {
+ @RestrictTo(LIBRARY_GROUP)
+ public boolean isEmojiSpanIndicatorEnabled() {
return mEmojiSpanIndicatorEnabled;
}
/**
- * @return whether a background should be drawn for the emoji.
+ * @return color of background drawn if {@link EmojiCompat#isEmojiSpanIndicatorEnabled} is true
* @hide
*/
- @RestrictTo(LIBRARY)
- @ColorInt int getEmojiSpanIndicatorColor() {
+ @RestrictTo(LIBRARY_GROUP)
+ public @ColorInt int getEmojiSpanIndicatorColor() {
return mEmojiSpanIndicatorColor;
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
index 5760f20..28f39ef 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/EmojiSpan.java
@@ -16,6 +16,7 @@
package androidx.emoji2.text;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.annotation.RestrictTo.Scope.TESTS;
import android.annotation.SuppressLint;
@@ -102,8 +103,9 @@
/**
* @hide
*/
- @RestrictTo(LIBRARY)
- final EmojiMetadata getMetadata() {
+ @NonNull
+ @RestrictTo(LIBRARY_GROUP)
+ public final EmojiMetadata getMetadata() {
return mMetadata;
}
@@ -122,8 +124,8 @@
*
* @hide
*/
- @RestrictTo(LIBRARY)
- final int getHeight() {
+ @RestrictTo(TESTS)
+ public final int getHeight() {
return mHeight;
}
diff --git a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
index 3549952..27afc5d 100644
--- a/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
+++ b/emoji2/emoji2/src/main/java/androidx/emoji2/text/MetadataRepo.java
@@ -79,12 +79,15 @@
}
/**
- * Construct MetadataRepo from a preloaded MetadatList.
+ * Construct MetadataRepo with empty metadata.
+ *
+ * This should only be used from tests.
+ * @hide
*/
@NonNull
- static MetadataRepo create(@NonNull final Typeface typeface,
- @NonNull final MetadataList metadataList) {
- return new MetadataRepo(typeface, metadataList);
+ @RestrictTo(RestrictTo.Scope.TESTS)
+ public static MetadataRepo create(@NonNull final Typeface typeface) {
+ return new MetadataRepo(typeface, new MetadataList());
}
/**
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index b850c56..8939d1c 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -298,6 +298,7 @@
method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+ method public void restoreBackStack(String);
method public void saveBackStack(String);
method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index 292c35f..1fb52f0 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -300,6 +300,7 @@
method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+ method public void restoreBackStack(String);
method public void saveBackStack(String);
method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
@@ -459,8 +460,14 @@
package androidx.fragment.app.strictmode {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentReuseViolation extends androidx.fragment.app.strictmode.Violation {
+ ctor public FragmentReuseViolation();
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentStrictMode {
method public static androidx.fragment.app.strictmode.FragmentStrictMode.Policy getDefaultPolicy();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentReuse(androidx.fragment.app.Fragment);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentTagUsage(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onRetainInstanceUsage(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onSetUserVisibleHint(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onTargetFragmentUsage(androidx.fragment.app.Fragment);
@@ -478,6 +485,8 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static final class FragmentStrictMode.Policy.Builder {
ctor public FragmentStrictMode.Policy.Builder();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy build();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentReuse();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentTagUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectRetainInstanceUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
@@ -486,6 +495,10 @@
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentTagUsageViolation extends androidx.fragment.app.strictmode.Violation {
+ ctor public FragmentTagUsageViolation();
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class RetainInstanceUsageViolation extends androidx.fragment.app.strictmode.Violation {
ctor public RetainInstanceUsageViolation();
}
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 86e9117..6abd717 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -305,6 +305,7 @@
method public void registerFragmentLifecycleCallbacks(androidx.fragment.app.FragmentManager.FragmentLifecycleCallbacks, boolean);
method public void removeFragmentOnAttachListener(androidx.fragment.app.FragmentOnAttachListener);
method public void removeOnBackStackChangedListener(androidx.fragment.app.FragmentManager.OnBackStackChangedListener);
+ method public void restoreBackStack(String);
method public void saveBackStack(String);
method public androidx.fragment.app.Fragment.SavedState? saveFragmentInstanceState(androidx.fragment.app.Fragment);
method public void setFragmentFactory(androidx.fragment.app.FragmentFactory);
@@ -485,8 +486,14 @@
package androidx.fragment.app.strictmode {
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentReuseViolation extends androidx.fragment.app.strictmode.Violation {
+ ctor public FragmentReuseViolation();
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentStrictMode {
method public static androidx.fragment.app.strictmode.FragmentStrictMode.Policy getDefaultPolicy();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentReuse(androidx.fragment.app.Fragment);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onFragmentTagUsage(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onRetainInstanceUsage(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onSetUserVisibleHint(androidx.fragment.app.Fragment);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static void onTargetFragmentUsage(androidx.fragment.app.Fragment);
@@ -504,6 +511,8 @@
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public static final class FragmentStrictMode.Policy.Builder {
ctor public FragmentStrictMode.Policy.Builder();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy build();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentReuse();
+ method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectFragmentTagUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectRetainInstanceUsage();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
@@ -512,6 +521,10 @@
method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
}
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class FragmentTagUsageViolation extends androidx.fragment.app.strictmode.Violation {
+ ctor public FragmentTagUsageViolation();
+ }
+
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY) public final class RetainInstanceUsageViolation extends androidx.fragment.app.strictmode.Violation {
ctor public RetainInstanceUsageViolation();
}
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 05dc225..115e903 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,6 +29,10 @@
buildTypes.all {
consumerProguardFiles "proguard-rules.pro"
}
+
+ defaultConfig {
+ multiDexEnabled true
+ }
}
dependencies {
@@ -57,6 +61,7 @@
androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+ androidTestImplementation(MULTIDEX)
androidTestImplementation(project(":internal-testutils-runtime"), {
exclude group: "androidx.fragment", module: "fragment"
})
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
index f082f79..b66133b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
@@ -48,8 +48,9 @@
setMaxLifecycle(fragment, Lifecycle.State.STARTED)
}
+ fragmentManager.fragmentStore.setSavedState(fragment.mWho, FragmentState(fragment))
val backStackState = BackStackState(
- listOf(FragmentState(fragment)),
+ listOf(fragment.mWho),
listOf(BackStackRecordState(backStackRecord))
)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
index 1a115e0..1fddf91 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
@@ -54,7 +54,8 @@
@Test
fun constructorFragmentFactory() {
val fragment = StrictFragment()
- val fragmentState = FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+ FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+ val fragmentState = fragmentStore.getSavedState(fragment.mWho)!!
val fragmentStateManager = FragmentStateManager(
dispatcher, fragmentStore,
@@ -71,7 +72,8 @@
@Test
fun constructorRetainedFragment() {
val fragment = StrictFragment()
- val fragmentState = FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+ FragmentStateManager(dispatcher, fragmentStore, fragment).saveState()
+ val fragmentState = fragmentStore.getSavedState(fragment.mWho)!!
assertThat(fragment.mSavedFragmentState)
.isNull()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
index 4c2457b..980468f 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
@@ -159,7 +159,7 @@
val savedActiveFragments = fragmentStore.saveActiveFragments()
assertThat(savedActiveFragments)
.hasSize(1)
- assertThat(savedActiveFragments[0].mWho)
+ assertThat(savedActiveFragments[0])
.isEqualTo(emptyFragment.mWho)
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
index d97a019..da3653d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
@@ -16,6 +16,7 @@
package androidx.fragment.app
+import android.os.Bundle
import androidx.fragment.app.test.FragmentTestActivity
import androidx.fragment.test.R
import androidx.test.core.app.ActivityScenario
@@ -23,6 +24,7 @@
import androidx.test.filters.MediumTest
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
@@ -55,8 +57,14 @@
fm.saveBackStack("replacement")
executePendingTransactions()
+ assertWithMessage("Saved Fragments should have their state saved")
+ .that(fragmentReplacement.calledOnSaveInstanceState)
+ .isTrue()
+
// Saved Fragments should be destroyed
- assertThat(fragmentReplacement.calledOnDestroy).isTrue()
+ assertWithMessage("Saved Fragments should be destroyed")
+ .that(fragmentReplacement.calledOnDestroy)
+ .isTrue()
}
}
@@ -220,4 +228,129 @@
}
}
}
+
+ @Test
+ fun restoreBackStack() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fm = withActivity {
+ supportFragmentManager
+ }
+ val fragmentBase = StrictFragment()
+ val fragmentReplacement = StateSaveFragment("saved", "unsaved")
+
+ fm.beginTransaction()
+ .add(R.id.content, fragmentBase)
+ .commit()
+ executePendingTransactions()
+
+ fm.beginTransaction()
+ .setReorderingAllowed(true)
+ .replace(R.id.content, fragmentReplacement)
+ .addToBackStack("replacement")
+ .commit()
+ executePendingTransactions()
+
+ fm.saveBackStack("replacement")
+ executePendingTransactions()
+
+ assertWithMessage("Saved Fragments should have their state saved")
+ .that(fragmentReplacement.calledOnSaveInstanceState)
+ .isTrue()
+
+ // Saved Fragments should be destroyed
+ assertWithMessage("Saved Fragments should be destroyed")
+ .that(fragmentReplacement.calledOnDestroy)
+ .isTrue()
+
+ fm.restoreBackStack("replacement")
+ executePendingTransactions()
+
+ val newFragmentReplacement = fm.findFragmentById(R.id.content)
+
+ assertThat(newFragmentReplacement).isInstanceOf(StateSaveFragment::class.java)
+
+ val stateSavedReplacement = newFragmentReplacement as StateSaveFragment
+
+ // Assert that restored fragment has its saved state restored
+ assertThat(stateSavedReplacement.savedState).isEqualTo("saved")
+ assertThat(stateSavedReplacement.unsavedState).isNull()
+ }
+ }
+
+ @Test
+ fun restoreBackStackAfterRecreate() {
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ var fm = withActivity {
+ supportFragmentManager
+ }
+ val fragmentBase = StrictFragment()
+ val fragmentReplacement = StateSaveFragment("saved", "unsaved")
+
+ fm.beginTransaction()
+ .add(R.id.content, fragmentBase)
+ .commit()
+ executePendingTransactions()
+
+ fm.beginTransaction()
+ .setReorderingAllowed(true)
+ .replace(R.id.content, fragmentReplacement)
+ .addToBackStack("replacement")
+ .commit()
+ executePendingTransactions()
+
+ fm.saveBackStack("replacement")
+ executePendingTransactions()
+
+ assertWithMessage("Saved Fragments should have their state saved")
+ .that(fragmentReplacement.calledOnSaveInstanceState)
+ .isTrue()
+
+ // Saved Fragments should be destroyed
+ assertWithMessage("Saved Fragments should be destroyed")
+ .that(fragmentReplacement.calledOnDestroy)
+ .isTrue()
+
+ // Now recreate the whole activity while the state of the back stack is saved
+ recreate()
+
+ fm = withActivity {
+ supportFragmentManager
+ }
+
+ fm.restoreBackStack("replacement")
+ executePendingTransactions()
+
+ val newFragmentReplacement = fm.findFragmentById(R.id.content)
+
+ assertThat(newFragmentReplacement).isInstanceOf(StateSaveFragment::class.java)
+
+ val stateSavedReplacement = newFragmentReplacement as StateSaveFragment
+
+ // Assert that restored fragment has its saved state restored
+ assertThat(stateSavedReplacement.savedState).isEqualTo("saved")
+ assertThat(stateSavedReplacement.unsavedState).isNull()
+ }
+ }
+
+ public class StateSaveFragment(
+ public var savedState: String? = null,
+ public val unsavedState: String? = null
+ ) : StrictFragment() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState != null) {
+ savedState = savedInstanceState.getString(STATE_KEY)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putString(STATE_KEY, savedState)
+ }
+
+ private companion object {
+ private const val STATE_KEY = "state"
+ }
+ }
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
index f2f100f..669e9f8d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/strictmode/FragmentStrictModeTest.kt
@@ -20,6 +20,7 @@
import androidx.fragment.app.StrictFragment
import androidx.fragment.app.executePendingTransactions
import androidx.fragment.app.test.FragmentTestActivity
+import androidx.fragment.test.R
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -129,6 +130,87 @@
}
@Test
+ public fun detectFragmentReuse() {
+ var violation: Violation? = null
+ val policy = FragmentStrictMode.Policy.Builder()
+ .detectFragmentReuse()
+ .penaltyListener { violation = it }
+ .build()
+ FragmentStrictMode.setDefaultPolicy(policy)
+
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fragmentManager = withActivity { supportFragmentManager }
+ val fragment = StrictFragment()
+
+ fragmentManager.beginTransaction()
+ .add(fragment, null)
+ .commit()
+ executePendingTransactions()
+
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commit()
+ executePendingTransactions()
+
+ fragmentManager.beginTransaction()
+ .add(fragment, null)
+ .commit()
+ executePendingTransactions()
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(violation).isInstanceOf(FragmentReuseViolation::class.java)
+ }
+ }
+
+ @Test
+ public fun detectFragmentReuseInFlightTransaction() {
+ var violation: Violation? = null
+ val policy = FragmentStrictMode.Policy.Builder()
+ .detectFragmentReuse()
+ .penaltyListener { violation = it }
+ .build()
+ FragmentStrictMode.setDefaultPolicy(policy)
+
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ val fragmentManager = withActivity { supportFragmentManager }
+ val fragment = StrictFragment()
+
+ fragmentManager.beginTransaction()
+ .add(fragment, null)
+ .commit()
+ executePendingTransactions()
+
+ fragmentManager.beginTransaction()
+ .remove(fragment)
+ .commit()
+ // Don't execute transaction here, keep it in-flight
+
+ fragmentManager.beginTransaction()
+ .add(fragment, null)
+ .commit()
+ executePendingTransactions()
+
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ assertThat(violation).isInstanceOf(FragmentReuseViolation::class.java)
+ }
+ }
+
+ @Test
+ public fun detectFragmentTagUsage() {
+ var violation: Violation? = null
+ val policy = FragmentStrictMode.Policy.Builder()
+ .detectFragmentTagUsage()
+ .penaltyListener { violation = it }
+ .build()
+ FragmentStrictMode.setDefaultPolicy(policy)
+
+ with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+ withActivity { setContentView(R.layout.activity_inflated_fragment) }
+ assertThat(violation).isInstanceOf(FragmentTagUsageViolation::class.java)
+ }
+ }
+
+ @Test
public fun detectRetainInstanceUsage() {
var violation: Violation? = null
val policy = FragmentStrictMode.Policy.Builder()
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
index 448ee87..68d5867 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackRecord.java
@@ -36,6 +36,7 @@
boolean mCommitted;
int mIndex = -1;
+ boolean mBeingSaved = false;
@Override
public String toString() {
@@ -280,6 +281,15 @@
}
}
+ void runOnExecuteRunnables() {
+ if (mExecuteRunnables != null) {
+ for (int i = 0; i < mExecuteRunnables.size(); i++) {
+ mExecuteRunnables.get(i).run();
+ }
+ mExecuteRunnables = null;
+ }
+ }
+
public void runOnCommitRunnables() {
if (mCommitRunnables != null) {
for (int i = 0; i < mCommitRunnables.size(); i++) {
@@ -404,6 +414,7 @@
final Op op = mOps.get(opNum);
final Fragment f = op.mFragment;
if (f != null) {
+ f.mBeingSaved = mBeingSaved;
f.setPopDirection(false);
f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
f.setNextTransition(mTransition);
@@ -467,6 +478,7 @@
final Op op = mOps.get(opNum);
Fragment f = op.mFragment;
if (f != null) {
+ f.mBeingSaved = mBeingSaved;
f.setPopDirection(true);
f.setAnimations(op.mEnterAnim, op.mExitAnim, op.mPopEnterAnim, op.mPopExitAnim);
f.setNextTransition(FragmentManager.reverseTransit(mTransition));
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
index 7184b5c..c102873 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/BackStackState.java
@@ -28,29 +28,33 @@
@SuppressLint("BanParcelableUsage")
class BackStackState implements Parcelable {
- final ArrayList<FragmentState> mFragments;
- final ArrayList<BackStackRecordState> mTransactions;
+ final List<String> mFragments;
+ final List<BackStackRecordState> mTransactions;
- BackStackState(List<FragmentState> fragments,
+ BackStackState(List<String> fragments,
List<BackStackRecordState> transactions) {
- mFragments = new ArrayList<>(fragments);
- mTransactions = new ArrayList<>(transactions);
+ mFragments = fragments;
+ mTransactions = transactions;
}
BackStackState(@NonNull Parcel in) {
- mFragments = in.createTypedArrayList(FragmentState.CREATOR);
+ mFragments = in.createStringArrayList();
mTransactions = in.createTypedArrayList(BackStackRecordState.CREATOR);
}
@NonNull
- ArrayList<BackStackRecord> instantiate(@NonNull FragmentManager fm) {
+ List<BackStackRecord> instantiate(@NonNull FragmentManager fm) {
// First instantiate the saved Fragments from state.
// These will populate the transactions we instantiate.
HashMap<String, Fragment> fragments = new HashMap<>(mFragments.size());
- for (FragmentState fragmentState : mFragments) {
- Fragment fragment = fragmentState.instantiate(fm.getFragmentFactory(),
- fm.getHost().getContext().getClassLoader());
- fragments.put(fragment.mWho, fragment);
+ for (String fWho : mFragments) {
+ // Retrieve any saved state, clearing it out for future calls
+ FragmentState fragmentState = fm.getFragmentStore().setSavedState(fWho, null);
+ if (fragmentState != null) {
+ Fragment fragment = fragmentState.instantiate(fm.getFragmentFactory(),
+ fm.getHost().getContext().getClassLoader());
+ fragments.put(fragment.mWho, fragment);
+ }
}
// Now instantiate all of the BackStackRecords
@@ -68,7 +72,7 @@
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeTypedList(mFragments);
+ dest.writeStringList(mFragments);
dest.writeTypedList(mTransactions);
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 7ca7617..6a65314 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -167,6 +167,8 @@
// If set this fragment is being removed from its activity.
boolean mRemoving;
+ boolean mBeingSaved;
+
// Set to true if this fragment was instantiated from a layout file.
boolean mFromLayout;
@@ -283,6 +285,10 @@
// track it separately.
boolean mIsCreated;
+ // True if the fragment was already added to a FragmentManager, but has since been removed
+ // again.
+ boolean mRemoved;
+
// Max Lifecycle state this Fragment can achieve.
Lifecycle.State mMaxState = Lifecycle.State.RESUMED;
@@ -2184,6 +2190,7 @@
mTag = null;
mHidden = false;
mDetached = false;
+ mRemoved = true;
}
/**
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
index d1702ac..7826175 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentLayoutInflaterFactory.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.R;
+import androidx.fragment.app.strictmode.FragmentStrictMode;
class FragmentLayoutInflaterFactory implements LayoutInflater.Factory2 {
private static final String TAG = FragmentManager.TAG;
@@ -130,6 +131,7 @@
+ "re-attached via the <fragment> tag: id=0x" + Integer.toHexString(id));
}
}
+ FragmentStrictMode.onFragmentTagUsage(fragment);
// Explicitly set the container for the fragment as we already know
// the parent that the fragment will be added to by the LayoutInflater
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 6484fae..05cf431 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -680,6 +680,22 @@
}
/**
+ * Restores the back stack previously saved via {@link #saveBackStack(String)}. This
+ * will result in all of the transactions that made up that back stack to be re-executed,
+ * thus re-adding any fragments that were added through those transactions. All state of
+ * those fragments will be restored as part of this process.
+ * <p>
+ * This function is asynchronous -- it enqueues the
+ * request to restore, but the action will not be performed until the application
+ * returns to its event loop.
+ *
+ * @param name The name of the back stack previously saved by {@link #saveBackStack(String)}.
+ */
+ public void restoreBackStack(@NonNull String name) {
+ enqueueAction(new RestoreBackStackState(name), false);
+ }
+
+ /**
* Save the back stack. While this functions similarly to
* {@link #popBackStack(String, int)}, it <strong>does not</strong> throw away the
* state of any fragments that were added through those transactions. Instead, the
@@ -1728,6 +1744,9 @@
}
FragmentStateManager addFragment(@NonNull Fragment fragment) {
+ if (fragment.mRemoved) {
+ FragmentStrictMode.onFragmentReuse(fragment);
+ }
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "add: " + fragment);
FragmentStateManager fragmentStateManager = createOrGetFragmentStateManager(fragment);
fragment.mFragmentManager = this;
@@ -2184,6 +2203,11 @@
}
executeOps(records, isRecordPop, startIndex, endIndex);
+ for (int recordNum = startIndex; recordNum < endIndex; recordNum++) {
+ final BackStackRecord record = records.get(recordNum);
+ record.runOnExecuteRunnables();
+ }
+
if (USE_STATE_MANAGER) {
// The last operation determines the overall direction, this ensures that operations
// such as push, push, pop, push are correctly considered a push
@@ -2599,9 +2623,24 @@
mBackStack.add(state);
}
+ boolean restoreBackStackState(@NonNull ArrayList<BackStackRecord> records,
+ @NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
+ BackStackState backStackState = mBackStackStates.get(name);
+ if (backStackState == null) {
+ return false;
+ }
+
+ List<BackStackRecord> backStackRecords = backStackState.instantiate(this);
+ for (BackStackRecord record : backStackRecords) {
+ records.add(record);
+ isRecordPop.add(false);
+ }
+ return true;
+ }
+
boolean saveBackStackState(@NonNull ArrayList<BackStackRecord> records,
@NonNull ArrayList<Boolean> isRecordPop, @NonNull String name) {
- int index = findBackStackIndex(name, -1, true);
+ final int index = findBackStackIndex(name, -1, true);
if (index < 0) {
return false;
}
@@ -2677,11 +2716,37 @@
}
// Now actually record each save
+ final ArrayList<String> fragments = new ArrayList<>();
+ for (Fragment f : allFragments) {
+ fragments.add(f.mWho);
+ }
+ final ArrayList<BackStackRecordState> backStackRecordStates =
+ new ArrayList<>(mBackStack.size() - index);
+ // Add placeholders for each BackStackRecordState
+ for (int i = index; i < mBackStack.size(); i++) {
+ backStackRecordStates.add(null);
+ }
+ final BackStackState backStackState = new BackStackState(
+ fragments, backStackRecordStates);
for (int i = mBackStack.size() - 1; i >= index; i--) {
- // TODO: Pre-process each BackStackRecord so that they actually save state
- records.add(mBackStack.remove(i));
+ final BackStackRecord record = mBackStack.remove(i);
+ record.mBeingSaved = true;
+ // Get a callback when the BackStackRecord is actually finished
+ final int currentIndex = i;
+ record.addOnExecuteRunnable(new Runnable() {
+ @Override
+ public void run() {
+ // First collapse the record to remove expanded ops and get it ready to save
+ record.collapseOps();
+ // Then save the state
+ BackStackRecordState state = new BackStackRecordState(record);
+ backStackRecordStates.set(currentIndex - index, state);
+ }
+ });
+ records.add(record);
isRecordPop.add(true);
}
+ mBackStackStates.put(name, backStackState);
return true;
}
@@ -2789,10 +2854,13 @@
mStateSaved = true;
mNonConfig.setIsStateSaved(true);
- // First collect all active fragments.
- ArrayList<FragmentState> active = mFragmentStore.saveActiveFragments();
+ // First save all active fragments.
+ ArrayList<String> active = mFragmentStore.saveActiveFragments();
- if (active.isEmpty()) {
+ // And grab all FragmentState objects
+ ArrayList<FragmentState> savedState = mFragmentStore.getAllSavedState();
+
+ if (savedState.isEmpty()) {
if (isLoggingEnabled(Log.VERBOSE)) Log.v(TAG, "saveAllState: no fragments!");
return null;
}
@@ -2817,6 +2885,7 @@
}
FragmentManagerState fms = new FragmentManagerState();
+ fms.mSavedState = savedState;
fms.mActive = active;
fms.mAdded = added;
fms.mBackStack = backStack;
@@ -2846,12 +2915,17 @@
// If there is no saved state at all, then there's nothing else to do
if (state == null) return;
FragmentManagerState fms = (FragmentManagerState) state;
- if (fms.mActive == null) return;
+ if (fms.mSavedState == null) return;
+
+ // Restore the saved state of all fragments
+ mFragmentStore.restoreSaveState(fms.mSavedState);
// Build the full list of active fragments, instantiating them from
// their saved state.
mFragmentStore.resetActiveFragments();
- for (FragmentState fs : fms.mActive) {
+ for (String who : fms.mActive) {
+ // Retrieve any saved state, clearing it out for future calls
+ FragmentState fs = mFragmentStore.setSavedState(who, null);
if (fs != null) {
FragmentStateManager fragmentStateManager;
Fragment retainedFragment = mNonConfig.findRetainedFragmentByWho(fs.mWho);
@@ -3712,6 +3786,21 @@
}
}
+ private class RestoreBackStackState implements OpGenerator {
+
+ private final String mName;
+
+ RestoreBackStackState(@NonNull String name) {
+ mName = name;
+ }
+
+ @Override
+ public boolean generateOps(@NonNull ArrayList<BackStackRecord> records,
+ @NonNull ArrayList<Boolean> isRecordPop) {
+ return restoreBackStackState(records, isRecordPop, mName);
+ }
+ }
+
private class SaveBackStackState implements OpGenerator {
private final String mName;
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
index ed56a59..a90909a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerState.java
@@ -25,7 +25,8 @@
@SuppressLint("BanParcelableUsage")
final class FragmentManagerState implements Parcelable {
- ArrayList<FragmentState> mActive;
+ ArrayList<FragmentState> mSavedState;
+ ArrayList<String> mActive;
ArrayList<String> mAdded;
BackStackRecordState[] mBackStack;
int mBackStackIndex;
@@ -40,7 +41,8 @@
}
public FragmentManagerState(Parcel in) {
- mActive = in.createTypedArrayList(FragmentState.CREATOR);
+ mSavedState = in.createTypedArrayList(FragmentState.CREATOR);
+ mActive = in.createStringArrayList();
mAdded = in.createStringArrayList();
mBackStack = in.createTypedArray(BackStackRecordState.CREATOR);
mBackStackIndex = in.readInt();
@@ -59,7 +61,8 @@
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeTypedList(mActive);
+ dest.writeTypedList(mSavedState);
+ dest.writeStringList(mActive);
dest.writeStringList(mAdded);
dest.writeTypedArray(mBackStack, flags);
dest.writeInt(mBackStackIndex);
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index b85f308..669c15a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -299,7 +299,9 @@
if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
Log.d(TAG, "movefrom ACTIVITY_CREATED: " + mFragment);
}
- if (mFragment.mView != null) {
+ if (mFragment.mBeingSaved) {
+ saveState();
+ } else if (mFragment.mView != null) {
// Need to save the current view state if not done already
// by saveInstanceState()
if (mFragment.mSavedViewState == null) {
@@ -323,6 +325,10 @@
mFragment.mState = Fragment.CREATED;
break;
case Fragment.ATTACHED:
+ if (mFragment.mBeingSaved
+ && mFragmentStore.getSavedState(mFragment.mWho) == null) {
+ saveState();
+ }
destroy();
break;
case Fragment.INITIALIZING:
@@ -617,8 +623,7 @@
mDispatcher.dispatchOnFragmentStopped(mFragment, false);
}
- @NonNull
- FragmentState saveState() {
+ void saveState() {
FragmentState fs = new FragmentState(mFragment);
if (mFragment.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
@@ -641,7 +646,7 @@
} else {
fs.mSavedFragmentState = mFragment.mSavedFragmentState;
}
- return fs;
+ mFragmentStore.setSavedState(mFragment.mWho, fs);
}
@Nullable
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
index 41bd97e..c7bfb2a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStore.java
@@ -36,6 +36,7 @@
private final ArrayList<Fragment> mAdded = new ArrayList<>();
private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();
+ private final HashMap<String, FragmentState> mSavedState = new HashMap<>();
private FragmentManagerViewModel mNonConfig;
@@ -167,18 +168,47 @@
values.removeAll(Collections.singleton(null));
}
+ @Nullable
+ FragmentState getSavedState(@NonNull String who) {
+ return mSavedState.get(who);
+ }
+
+ /**
+ * Sets the saved state, returning the previously set FragmentState, if any.
+ */
+ @Nullable
+ FragmentState setSavedState(@NonNull String who, @Nullable FragmentState fragmentState) {
+ if (fragmentState != null) {
+ return mSavedState.put(who, fragmentState);
+ } else {
+ return mSavedState.remove(who);
+ }
+ }
+
+ void restoreSaveState(@NonNull ArrayList<FragmentState> savedState) {
+ mSavedState.clear();
+ for (FragmentState fs : savedState) {
+ mSavedState.put(fs.mWho, fs);
+ }
+ }
+
@NonNull
- ArrayList<FragmentState> saveActiveFragments() {
- ArrayList<FragmentState> active = new ArrayList<>(mActive.size());
+ ArrayList<FragmentState> getAllSavedState() {
+ return new ArrayList<>(mSavedState.values());
+ }
+
+ @NonNull
+ ArrayList<String> saveActiveFragments() {
+ ArrayList<String> active = new ArrayList<>(mActive.size());
for (FragmentStateManager fragmentStateManager : mActive.values()) {
if (fragmentStateManager != null) {
Fragment f = fragmentStateManager.getFragment();
- FragmentState fs = fragmentStateManager.saveState();
- active.add(fs);
+ fragmentStateManager.saveState();
+ active.add(f.mWho);
if (FragmentManager.isLoggingEnabled(Log.VERBOSE)) {
- Log.v(TAG, "Saved state of " + f + ": " + fs.mSavedFragmentState);
+ Log.v(TAG, "Saved state of " + f + ": " + f.mSavedFragmentState);
}
}
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
index d6a2636..7e79244 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentTransaction.java
@@ -32,6 +32,7 @@
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;
import androidx.core.view.ViewCompat;
+import androidx.fragment.app.strictmode.FragmentStrictMode;
import androidx.lifecycle.Lifecycle;
import java.lang.annotation.Retention;
@@ -121,6 +122,7 @@
ArrayList<String> mSharedElementTargetNames;
boolean mReorderingAllowed = false;
+ ArrayList<Runnable> mExecuteRunnables;
ArrayList<Runnable> mCommitRunnables;
/**
@@ -253,6 +255,9 @@
}
void doAddOp(int containerViewId, Fragment fragment, @Nullable String tag, int opcmd) {
+ if (fragment.mRemoved) {
+ FragmentStrictMode.onFragmentReuse(fragment);
+ }
final Class<?> fragmentClass = fragment.getClass();
final int modifiers = fragmentClass.getModifiers();
if (fragmentClass.isAnonymousClass() || !Modifier.isPublic(modifiers)
@@ -821,6 +826,18 @@
}
/**
+ * Add a runnable that is run immediately after the transaction is executed.
+ * This differs from the commit runnables in that it happens before any
+ * fragments move to their expected state.
+ */
+ void addOnExecuteRunnable(@NonNull Runnable runnable) {
+ if (mExecuteRunnables == null) {
+ mExecuteRunnables = new ArrayList<>();
+ }
+ mExecuteRunnables.add(runnable);
+ }
+
+ /**
* Add a Runnable to this transaction that will be run after this transaction has
* been committed. If fragment transactions are {@link #setReorderingAllowed(boolean) optimized}
* this may be after other subsequent fragment operations have also taken place, or operations
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
index e022dc3..f209c61 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentReuseViolation.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.fragment.app.strictmode;
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.RestrictTo;
-/** @hide */
-oneway interface IOnInputCompletedListener {
- void onInputCompleted(String value, IOnDoneCallback callback) = 1;
+/** See #{@link FragmentStrictMode.Policy.Builder#detectFragmentReuse()}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // TODO: Make API public as soon as we have a few checks
+public final class FragmentReuseViolation extends Violation {
}
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
index c4d5f18..1a554d7 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.java
@@ -51,6 +51,8 @@
PENALTY_LOG,
PENALTY_DEATH,
+ DETECT_FRAGMENT_REUSE,
+ DETECT_FRAGMENT_TAG_USAGE,
DETECT_RETAIN_INSTANCE_USAGE,
DETECT_SET_USER_VISIBLE_HINT,
DETECT_TARGET_FRAGMENT_USAGE,
@@ -145,6 +147,25 @@
}
/**
+ * Detects cases, where a #{@link Fragment} instance is reused, after it was previously
+ * removed from a #{@link FragmentManager}.
+ */
+ @NonNull
+ @SuppressLint("BuilderSetStyle")
+ public Builder detectFragmentReuse() {
+ flags.add(Flag.DETECT_FRAGMENT_REUSE);
+ return this;
+ }
+
+ /** Detects usage of the <fragment> tag inside XML layouts. */
+ @NonNull
+ @SuppressLint("BuilderSetStyle")
+ public Builder detectFragmentTagUsage() {
+ flags.add(Flag.DETECT_FRAGMENT_TAG_USAGE);
+ return this;
+ }
+
+ /**
* Detects calls to #{@link Fragment#setRetainInstance} and
* #{@link Fragment#getRetainInstance()}.
*/
@@ -220,6 +241,22 @@
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static void onFragmentReuse(@NonNull Fragment fragment) {
+ Policy policy = getNearestPolicy(fragment);
+ if (policy.flags.contains(Flag.DETECT_FRAGMENT_REUSE)) {
+ handlePolicyViolation(fragment, policy, new FragmentReuseViolation());
+ }
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static void onFragmentTagUsage(@NonNull Fragment fragment) {
+ Policy policy = getNearestPolicy(fragment);
+ if (policy.flags.contains(Flag.DETECT_FRAGMENT_TAG_USAGE)) {
+ handlePolicyViolation(fragment, policy, new FragmentTagUsageViolation());
+ }
+ }
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onRetainInstanceUsage(@NonNull Fragment fragment) {
Policy policy = getNearestPolicy(fragment);
if (policy.flags.contains(Flag.DETECT_RETAIN_INSTANCE_USAGE)) {
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
similarity index 65%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
index e022dc3..0084e18 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentTagUsageViolation.java
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.fragment.app.strictmode;
-import androidx.car.app.IOnDoneCallback;
+import androidx.annotation.RestrictTo;
-/** @hide */
-oneway interface IOnInputCompletedListener {
- void onInputCompleted(String value, IOnDoneCallback callback) = 1;
+/** See #{@link FragmentStrictMode.Policy.Builder#detectFragmentTagUsage()}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // TODO: Make API public as soon as we have a few checks
+public final class FragmentTagUsageViolation extends Violation {
}
diff --git a/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml b/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
index 50f1a690..8503ecd 100644
--- a/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
+++ b/hilt/integration-tests/workerapp/src/main/AndroidManifest.xml
@@ -36,7 +36,7 @@
tools:node="merge">
<!-- Remove initializer for WorkManager on-demand initialization -->
<meta-data
- android:name="androidx.work.impl.WorkManagerInitializer"
+ android:name="androidx.work.WorkManagerInitializer"
android:value="@string/androidx_startup"
tools:node="remove" />
</provider>
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 63486ac..32e4434 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -13,7 +13,7 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
api("androidx.leanback:leanback:1.1.0-beta01")
- api(project(":paging:paging-runtime"))
+ api("androidx.paging:paging-runtime:3.0.0-beta03")
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/leanback/leanback-preference/build.gradle b/leanback/leanback-preference/build.gradle
index 5214770..3559967 100644
--- a/leanback/leanback-preference/build.gradle
+++ b/leanback/leanback-preference/build.gradle
@@ -13,7 +13,7 @@
api("androidx.appcompat:appcompat:1.0.0")
api("androidx.recyclerview:recyclerview:1.0.0")
api("androidx.preference:preference:1.1.0")
- api("androidx.leanback:leanback:1.1.0-beta01")
+ api(project(":leanback:leanback"))
}
android {
diff --git a/leanback/leanback/build.gradle b/leanback/leanback/build.gradle
index 25216be..e0f38fa 100644
--- a/leanback/leanback/build.gradle
+++ b/leanback/leanback/build.gradle
@@ -15,7 +15,7 @@
implementation("androidx.collection:collection:1.0.0")
api("androidx.media:media:1.0.0")
api("androidx.fragment:fragment:1.0.0")
- api("androidx.recyclerview:recyclerview:1.2.0-beta01")
+ api("androidx.recyclerview:recyclerview:1.2.0-rc01")
api("androidx.appcompat:appcompat:1.0.0")
androidTestImplementation(KOTLIN_STDLIB)
diff --git a/lifecycle/OWNERS b/lifecycle/OWNERS
index 77b5892..0982d74 100644
--- a/lifecycle/OWNERS
+++ b/lifecycle/OWNERS
@@ -1,5 +1,6 @@
[email protected]
[email protected]
[email protected]
per-file settings.gradle = [email protected], [email protected]
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
index 31c31a6..098de7c 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/main/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetector.kt
@@ -98,7 +98,9 @@
// Given the field `val liveDataField = MutableLiveData<Boolean>()`
// expression: `MutableLiveData<Boolean>()`
// argument: `Boolean`
- val expression = element.sourcePsi?.children?.get(0) as? KtCallExpression
+ val expression = element.sourcePsi
+ ?.children
+ ?.firstOrNull { it is KtCallExpression } as? KtCallExpression
val argument = expression?.typeArguments?.singleOrNull()
return argument?.typeReference
}
diff --git a/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt b/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
index bbb2490..095d9f6 100644
--- a/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
+++ b/lifecycle/lifecycle-livedata-core-ktx-lint/src/test/java/androidx/lifecycle/lint/NonNullableMutableLiveDataDetectorTest.kt
@@ -582,6 +582,167 @@
}
@Test
+ fun modifiersFieldTest() {
+ check(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.lifecycle.LiveData
+ import androidx.lifecycle.MutableLiveData
+
+ class MyClass1 {
+ internal val firstLiveDataField = MutableLiveData<Boolean>()
+ protected val secondLiveDataField = MutableLiveData<Boolean?>()
+
+ fun foo() {
+ firstLiveDataField.value = false
+ firstLiveDataField.value = null
+ secondLiveDataField.value = null
+ secondLiveDataField.value = false
+ }
+ }
+ """
+ ).indented()
+ ).expect(
+ """
+src/com/example/MyClass1.kt:12: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+ firstLiveDataField.value = null
+ ~~~~
+1 errors, 0 warnings
+ """
+ ).expectFixDiffs(
+ """
+Fix for src/com/example/MyClass1.kt line 12: Change `LiveData` type to nullable:
+@@ -7 +7
+- internal val firstLiveDataField = MutableLiveData<Boolean>()
++ internal val firstLiveDataField = MutableLiveData<Boolean?>()
+ """
+ )
+ }
+
+ @Test
+ fun implementationClassTest() {
+ check(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.lifecycle.LiveData
+ import androidx.lifecycle.MutableLiveData
+
+ interface MyClass2 {
+ val firstLiveDataField : LiveData<Boolean>
+ val secondLiveDataField : LiveData<Boolean?>
+ val thirdLiveDataField : LiveData<Boolean?>
+ val fourLiveDataField : LiveData<List<Boolean>?>
+ val fiveLiveDataField : LiveData<List<Boolean>?>
+ }
+
+ class MyClass1 : MyClass2 {
+ override val firstLiveDataField = MutableLiveData<Boolean>()
+ override val secondLiveDataField = MutableLiveData<Boolean?>()
+ override val thirdLiveDataField = MutableLiveData<Boolean?>(null)
+ override val fourLiveDataField = MutableLiveData<List<Boolean>?>(null)
+ override val fiveLiveDataField : MutableLiveData<List<Boolean>?> = MutableLiveData(null)
+
+ fun foo() {
+ firstLiveDataField.value = false
+ firstLiveDataField.value = null
+ secondLiveDataField.value = null
+ secondLiveDataField.value = false
+ thirdLiveDataField.value = null
+ thirdLiveDataField.value = false
+ fourLiveDataField.value = null
+ fourLiveDataField.value = emptyList()
+ fiveLiveDataField.value = null
+ fiveLiveDataField.value = emptyList()
+ }
+ }
+ """
+ ).indented()
+ ).expect(
+ """
+src/com/example/MyClass2.kt:23: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+ firstLiveDataField.value = null
+ ~~~~
+1 errors, 0 warnings
+ """
+ ).expectFixDiffs(
+ """
+Fix for src/com/example/MyClass2.kt line 23: Change `LiveData` type to nullable:
+@@ -15 +15
+- override val firstLiveDataField = MutableLiveData<Boolean>()
++ override val firstLiveDataField = MutableLiveData<Boolean?>()
+ """
+ )
+ }
+
+ @Test
+ fun extendClassTest() {
+ check(
+ kotlin(
+ """
+ package com.example
+
+ import androidx.lifecycle.LiveData
+ import androidx.lifecycle.MutableLiveData
+
+ abstract class MyClass2 {
+ val firstLiveDataField : LiveData<Boolean>
+ val secondLiveDataField : LiveData<Boolean?>
+ val thirdLiveDataField : LiveData<Boolean?>
+ val fourLiveDataField : LiveData<List<Boolean>?>
+ val fiveLiveDataField : LiveData<List<Boolean>>
+ }
+
+ class MyClass1 : MyClass2() {
+ override val firstLiveDataField = MutableLiveData<Boolean>()
+ override val secondLiveDataField = MutableLiveData<Boolean?>()
+ override val thirdLiveDataField = MutableLiveData<Boolean?>(null)
+ override val fourLiveDataField = MutableLiveData<List<Boolean>?>(null)
+ override val fiveLiveDataField = MutableLiveData<List<Boolean>>()
+
+ fun foo() {
+ firstLiveDataField.value = false
+ firstLiveDataField.value = null
+ secondLiveDataField.value = null
+ secondLiveDataField.value = false
+ thirdLiveDataField.value = null
+ thirdLiveDataField.value = false
+ fourLiveDataField.value = null
+ fourLiveDataField.value = emptyList()
+ fiveLiveDataField.value = null
+ fiveLiveDataField.value = emptyList()
+ }
+ }
+ """
+ ).indented()
+ ).expect(
+ """
+src/com/example/MyClass2.kt:23: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+ firstLiveDataField.value = null
+ ~~~~
+src/com/example/MyClass2.kt:30: Error: Cannot set non-nullable LiveData value to null [NullSafeMutableLiveData]
+ fiveLiveDataField.value = null
+ ~~~~
+2 errors, 0 warnings
+ """
+ ).expectFixDiffs(
+ """
+Fix for src/com/example/MyClass2.kt line 23: Change `LiveData` type to nullable:
+@@ -15 +15
+- override val firstLiveDataField = MutableLiveData<Boolean>()
++ override val firstLiveDataField = MutableLiveData<Boolean?>()
+Fix for src/com/example/MyClass2.kt line 30: Change `LiveData` type to nullable:
+@@ -19 +19
+- override val fiveLiveDataField = MutableLiveData<List<Boolean>>()
++ override val fiveLiveDataField = MutableLiveData<List<Boolean>?>()
+ """
+ )
+ }
+
+ @Test
fun objectLiveData() {
check(
kotlin(
diff --git a/lifecycle/lifecycle-process/api/current.txt b/lifecycle/lifecycle-process/api/current.txt
index 29e1211..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/current.txt
+++ b/lifecycle/lifecycle-process/api/current.txt
@@ -1,6 +1,12 @@
// Signature format: 4.0
package androidx.lifecycle {
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ ctor public ProcessLifecycleInitializer();
+ method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
method public static androidx.lifecycle.LifecycleOwner get();
method public androidx.lifecycle.Lifecycle getLifecycle();
diff --git a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
index 29e1211..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-process/api/public_plus_experimental_current.txt
@@ -1,6 +1,12 @@
// Signature format: 4.0
package androidx.lifecycle {
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ ctor public ProcessLifecycleInitializer();
+ method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
public class ProcessLifecycleOwner implements androidx.lifecycle.LifecycleOwner {
method public static androidx.lifecycle.LifecycleOwner get();
method public androidx.lifecycle.Lifecycle getLifecycle();
diff --git a/lifecycle/lifecycle-process/api/restricted_current.txt b/lifecycle/lifecycle-process/api/restricted_current.txt
index 25bd0dd0..429b2b2 100644
--- a/lifecycle/lifecycle-process/api/restricted_current.txt
+++ b/lifecycle/lifecycle-process/api/restricted_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.lifecycle {
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
+ public final class ProcessLifecycleInitializer implements androidx.startup.Initializer<androidx.lifecycle.LifecycleOwner> {
ctor public ProcessLifecycleInitializer();
method public androidx.lifecycle.LifecycleOwner create(android.content.Context);
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
index 9fad75d..03e32bf 100644
--- a/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
+++ b/lifecycle/lifecycle-process/src/main/java/androidx/lifecycle/ProcessLifecycleInitializer.java
@@ -19,19 +19,15 @@
import android.content.Context;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.startup.Initializer;
import java.util.Collections;
import java.util.List;
/**
- * Internal class to initialize Lifecycles.
- *
- * @hide
+ * Initializes {@link ProcessLifecycleOwner} using {@code androidx.startup}.
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ProcessLifecycleInitializer implements Initializer<LifecycleOwner> {
+public final class ProcessLifecycleInitializer implements Initializer<LifecycleOwner> {
@NonNull
@Override
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index f546dff..1d0082c 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -51,7 +51,7 @@
androidx {
name = "Lifecycle ViewModel Compose"
publish = Publish.SNAPSHOT_AND_RELEASE
- mavenVersion = LibraryVersions.LIFECYCLE_COMPOSE
+ mavenVersion = LibraryVersions.LIFECYCLE_VIEWMODEL_COMPOSE
mavenGroup = LibraryGroups.LIFECYCLE
inceptionYear = "2021"
description = "Compose integration with Lifecycle ViewModel"
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index c581d23..e4974d0 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -143,7 +143,7 @@
* If the given {@code newValue} is {@link Closeable},
* it will be closed once {@link #clear()}.
* <p>
- * If a value was already set for the given key, this calls do nothing and
+ * If a value was already set for the given key, this call does nothing and
* returns currently associated value, the given {@code newValue} would be ignored
* <p>
* If the ViewModel was already cleared then close() would be called on the returned object if
diff --git a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
index 77b03f5e..afb8ca9 100644
--- a/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
+++ b/media2/media2-session/src/androidTest/java/androidx/media2/session/MediaControllerTest.java
@@ -61,6 +61,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.filters.LargeTest;
+import androidx.test.filters.Suppress;
import androidx.testutils.PollingCheck;
import org.junit.After;
@@ -907,6 +908,7 @@
}
@Test
+ @Suppress // b/183700008
public void setVolumeWithLocalVolume() throws Exception {
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
// This test is not eligible for this device.
@@ -951,6 +953,7 @@
}
@Test
+ @Suppress // b/183700008
public void adjustVolumeWithLocalVolume() throws Exception {
if (Build.VERSION.SDK_INT >= 21 && mAudioManager.isVolumeFixed()) {
// This test is not eligible for this device.
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 5aac8d1..088c60e 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -1049,4 +1049,66 @@
.that(matchArgs?.getInt("id"))
.isEqualTo(id)
}
+
+ @Test
+ fun deepLinkCaseInsensitiveDomainWithPath() {
+ val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users/{id}/posts"
+ val deepLink = NavDeepLink(deepLinkArgument)
+
+ val id = 2
+ val matchArgs = deepLink.getMatchingArguments(
+ Uri.parse("${DEEP_LINK_EXACT_HTTPS.toUpperCase()}/users/$id/posts"),
+ mapOf("id" to intArgument())
+ )
+ assertWithMessage("Args should not be null")
+ .that(matchArgs)
+ .isNotNull()
+ assertWithMessage("Args should contain the id")
+ .that(matchArgs?.getInt("id"))
+ .isEqualTo(id)
+ }
+
+ @Test
+ fun deepLinkCaseInsensitivePath() {
+ val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users/{id}/posts"
+ val deepLink = NavDeepLink(deepLinkArgument)
+
+ val id = 2
+ val matchArgs = deepLink.getMatchingArguments(
+ Uri.parse(
+ deepLinkArgument
+ .replace("{id}", id.toString())
+ .replace("users", "Users")
+ ),
+ mapOf("id" to intArgument())
+ )
+ assertWithMessage("Args should not be null")
+ .that(matchArgs)
+ .isNotNull()
+ assertWithMessage("Args should contain the id")
+ .that(matchArgs?.getInt("id"))
+ .isEqualTo(id)
+ }
+
+ @Test
+ fun deepLinkCaseSensitiveQueryParams() {
+ val deepLinkString = "$DEEP_LINK_EXACT_HTTP/?myParam={param}"
+ val deepLink = NavDeepLink(deepLinkString)
+
+ val param = 2
+ val deepLinkUpper = deepLinkString
+ .replace("myParam", "MYPARAM")
+ .replace("{param}", param.toString())
+ val matchArgs = deepLink.getMatchingArguments(
+ Uri.parse(deepLinkUpper),
+ mapOf("param" to intArgument())
+ )
+
+ assertWithMessage("Args should be not be null")
+ .that(matchArgs)
+ .isNotNull()
+ assertWithMessage("Args bundle should be empty")
+ .that(matchArgs?.isEmpty)
+ .isTrue()
+ }
}
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
index 9bf2dc9..06c2482 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavAction.kt
@@ -63,7 +63,7 @@
/**
* Sets the argument bundle to be used by default when navigating to this action.
*
- * @param defaultArgs argument bundle that should be used by default
+ * @param defaultArguments argument bundle that should be used by default
*/
public var defaultArguments: Bundle? = null
)
\ No newline at end of file
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
index 302b2f7..42d18c1 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
@@ -422,7 +422,7 @@
// specifically escape any .* instances to ensure
// they are still treated as wildcards in our final regex
val finalRegex = uriRegex.toString().replace(".*", "\\E.*\\Q")
- pattern = Pattern.compile(finalRegex)
+ pattern = Pattern.compile(finalRegex, Pattern.CASE_INSENSITIVE)
}
if (mimeType != null) {
val mimeTypePattern = Pattern.compile("^[\\s\\S]+/[\\s\\S]+$")
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index 426e287..3d2eca2 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -195,9 +195,9 @@
*
* @param deepLink to the destination reachable from the current NavGraph
* @return True if the deepLink exists for the destination.
- * @see .addDeepLink
+ * @see NavDestination.addDeepLink
* @see NavController.navigate
- * @see .hasDeepLink
+ * @see NavDestination.hasDeepLink
*/
public open fun hasDeepLink(deepLink: Uri): Boolean {
return hasDeepLink(NavDeepLinkRequest(deepLink, null, null))
@@ -214,7 +214,7 @@
*
* @param deepLinkRequest to the destination reachable from the current NavGraph
* @return True if the deepLink exists for the destination.
- * @see .addDeepLink
+ * @see NavDestination.addDeepLink
* @see NavController.navigate
*/
public open fun hasDeepLink(deepLinkRequest: NavDeepLinkRequest): Boolean {
@@ -249,7 +249,7 @@
* @param uriPattern The uri pattern to add as a deep link
* @see NavController.handleDeepLink
* @see NavController.navigate
- * @see .addDeepLink
+ * @see NavDestination.addDeepLink
*/
public fun addDeepLink(uriPattern: String) {
addDeepLink(NavDeepLink.Builder().setUriPattern(uriPattern).build())
@@ -367,7 +367,7 @@
/**
* @return Whether this NavDestination supports outgoing actions
- * @see .putAction
+ * @see NavDestination.putAction
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open fun supportsActions(): Boolean {
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
index d21e3be..091afc1 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavOptions.kt
@@ -78,7 +78,7 @@
* Whether the destination set in [.getPopUpTo] should be popped from the back stack.
* @see Builder.setPopUpTo
*
- * @see .getPopUpTo
+ * @see NavOptions.getPopUpTo
*/
public fun isPopUpToInclusive(): Boolean {
return popUpToInclusive
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
index 40858ec..3051250 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavigatorProvider.kt
@@ -41,7 +41,7 @@
* [Navigator.Name annotation][Navigator.Name]
* @throws IllegalStateException if the Navigator has not been added
*
- * @see .addNavigator
+ * @see NavigatorProvider.addNavigator
*/
public fun <T : Navigator<*>> getNavigator(navigatorClass: Class<T>): T {
val name = getNameForNavigator(navigatorClass)
@@ -56,7 +56,7 @@
*
* @throws IllegalStateException if the Navigator has not been added
*
- * @see .addNavigator
+ * @see NavigatorProvider.addNavigator
*/
@Suppress("UNCHECKED_CAST")
@CallSuper
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index d384453..f7e8faf 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -36,8 +36,8 @@
api(projectOrArtifact(":compose:runtime:runtime"))
api(projectOrArtifact(":compose:runtime:runtime-saveable"))
api(projectOrArtifact(":compose:ui:ui"))
- api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose"))
- api(prebuiltOrSnapshot("androidx.navigation:navigation-runtime-ktx:2.3.4"))
+ api("androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03")
+ api("androidx.navigation:navigation-runtime-ktx:2.3.4")
androidTestImplementation(projectOrArtifact(":compose:material:material"))
androidTestImplementation("androidx.navigation:navigation-testing:2.3.1")
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
index 8412469..7c9a26b 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavControllerTest.kt
@@ -25,6 +25,8 @@
import android.os.Parcelable
import android.view.View
import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.addCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.testing.TestLifecycleOwner
@@ -323,6 +325,10 @@
navController.navigate(deepLink)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent = navigator.current.second?.getParcelable<Intent>(
+ NavController.KEY_DEEP_LINK_INTENT
+ )
+ assertThat(intent?.data).isEqualTo(deepLink)
}
@UiThreadTest
@@ -348,11 +354,16 @@
val navController = createNavController()
navController.setGraph(R.navigation.nav_simple)
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
- val deepLink = NavDeepLinkRequest(null, "test.action", null)
+ val action = "test.action"
+ val deepLink = NavDeepLinkRequest(null, action, null)
navController.navigate(deepLink)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.second_test)
assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent = navigator.current.second?.getParcelable<Intent>(
+ NavController.KEY_DEEP_LINK_INTENT
+ )
+ assertThat(intent?.action).isEqualTo(action)
}
@UiThreadTest
@@ -387,11 +398,16 @@
val navController = createNavController()
navController.setGraph(R.navigation.nav_deeplink)
val navigator = navController.navigatorProvider.getNavigator(TestNavigator::class.java)
- val deepLink = NavDeepLinkRequest(null, null, "type/test")
+ val mimeType = "type/test"
+ val deepLink = NavDeepLinkRequest(null, null, mimeType)
navController.navigate(deepLink)
assertThat(navController.currentDestination?.id ?: 0).isEqualTo(R.id.forth_test)
assertThat(navigator.backStack.size).isEqualTo(2)
+ val intent = navigator.current.second?.getParcelable<Intent>(
+ NavController.KEY_DEEP_LINK_INTENT
+ )
+ assertThat(intent?.type).isEqualTo(mimeType)
}
@UiThreadTest
@@ -1580,6 +1596,35 @@
.that(collectedDestinationIds).hasSize(1)
}
+ @UiThreadTest
+ @Test
+ fun testSetOnBackPressedDispatcherOnNavBackStackEntry() {
+ var backPressedIntercepted = false
+ val navController = createNavController()
+ val lifecycleOwner = TestLifecycleOwner()
+ val dispatcher = OnBackPressedDispatcher()
+
+ navController.setLifecycleOwner(lifecycleOwner)
+ navController.setOnBackPressedDispatcher(dispatcher)
+
+ navController.setGraph(R.navigation.nav_simple)
+ navController.navigate(R.id.second_test)
+ assertEquals(R.id.start_test, navController.previousBackStackEntry?.destination?.id ?: 0)
+
+ dispatcher.addCallback(navController.currentBackStackEntry!!) {
+ backPressedIntercepted = true
+ }
+
+ // Move to STOPPED
+ lifecycleOwner.currentState = Lifecycle.State.CREATED
+ // Move back up to RESUMED
+ lifecycleOwner.currentState = Lifecycle.State.RESUMED
+
+ dispatcher.onBackPressed()
+
+ assertThat(backPressedIntercepted).isTrue()
+ }
+
private fun createNavController(): NavController {
val navController = NavController(ApplicationProvider.getApplicationContext())
val navigator = TestNavigator()
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
index b8d2ea9..532a73a 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/ActivityNavigator.kt
@@ -235,7 +235,7 @@
* @param dataPattern A URI pattern with segments in the form of `{argName}` that
* will be replaced with URI encoded versions of the Strings in the
* arguments Bundle.
- * @see .setData
+ * @see Destination.setData
*
* @return this [Destination]
*/
@@ -371,7 +371,7 @@
* present.
*
* @param data A static URI that should always be used.
- * @see .setDataPattern
+ * @see Destination.setDataPattern
* @return this [Destination]
*/
public fun setData(data: Uri?): Destination {
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 c4d6a9e..1eabbd2 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -65,7 +65,7 @@
/**
* Gets the topmost navigation graph associated with this NavController.
*
- * @see .setGraph
+ * @see NavController.setGraph
* @throws IllegalStateException if called before `setGraph()`.
*/
get() {
@@ -80,8 +80,8 @@
* The graph can be retrieved later via [.getGraph].
*
* @param graph graph to set
- * @see .setGraph
- * @see .getGraph
+ * @see NavController.setGraph
+ * @see NavController.getGraph
*/
@CallSuper
set(graph) {
@@ -500,9 +500,9 @@
*
* @param graphResId resource id of the navigation graph to inflate
*
- * @see .getNavInflater
- * @see .setGraph
- * @see .getGraph
+ * @see NavController.getNavInflater
+ * @see NavController.setGraph
+ * @see NavController.getGraph
*/
@CallSuper
public open fun setGraph(@NavigationRes graphResId: Int) {
@@ -518,9 +518,9 @@
* @param graphResId resource id of the navigation graph to inflate
* @param startDestinationArgs arguments to send to the start destination of the graph
*
- * @see .getNavInflater
- * @see .setGraph
- * @see .getGraph
+ * @see NavController.getNavInflater
+ * @see NavController.setGraph
+ * @see NavController.getGraph
*/
@CallSuper
public open fun setGraph(@NavigationRes graphResId: Int, startDestinationArgs: Bundle?) {
@@ -534,8 +534,8 @@
* The graph can be retrieved later via [.getGraph].
*
* @param graph graph to set
- * @see .setGraph
- * @see .getGraph
+ * @see NavController.setGraph
+ * @see NavController.getGraph
*/
@CallSuper
public open fun setGraph(graph: NavGraph, startDestinationArgs: Bundle?) {
@@ -940,7 +940,7 @@
* thrown.
*
* @param deepLink deepLink to the destination reachable from the current NavGraph
- * @see .navigate
+ * @see NavController.navigate
*/
public open fun navigate(deepLink: Uri) {
navigate(NavDeepLinkRequest(deepLink, null, null))
@@ -955,7 +955,7 @@
*
* @param deepLink deepLink to the destination reachable from the current NavGraph
* @param navOptions special options for this navigation operation
- * @see .navigate
+ * @see NavController.navigate
*/
public open fun navigate(deepLink: Uri, navOptions: NavOptions?) {
navigate(NavDeepLinkRequest(deepLink, null, null), navOptions, null)
@@ -971,7 +971,7 @@
* @param deepLink deepLink to the destination reachable from the current NavGraph
* @param navOptions special options for this navigation operation
* @param navigatorExtras extras to pass to the Navigator
- * @see .navigate
+ * @see NavController.navigate
*/
public open fun navigate(
deepLink: Uri,
@@ -1033,8 +1033,13 @@
val deepLinkMatch = _graph!!.matchDeepLink(request)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
- val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs)
+ val args = destination.addInDefaultArgs(deepLinkMatch.matchingArgs) ?: Bundle()
val node = deepLinkMatch.destination
+ val intent = Intent().apply {
+ setDataAndType(request.uri, request.mimeType)
+ action = request.action
+ }
+ args.putParcelable(KEY_DEEP_LINK_INTENT, intent)
navigate(node, args, navOptions, navigatorExtras)
} else {
throw IllegalArgumentException(
@@ -1275,6 +1280,13 @@
onBackPressedCallback.remove()
// Then add it to the new dispatcher
dispatcher.addCallback(lifecycleOwner!!, onBackPressedCallback)
+
+ // Make sure that listener for updating the NavBackStackEntry lifecycles comes after
+ // the dispatcher
+ lifecycleOwner!!.lifecycle.apply {
+ removeObserver(lifecycleObserver)
+ addObserver(lifecycleObserver)
+ }
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
index 10af220..2b65a53 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavDeepLinkBuilder.kt
@@ -52,7 +52,7 @@
* default activity to launch, if available.
*
* @param context Context used to create deep links
- * @see .setComponentName
+ * @see NavDeepLinkBuilder.setComponentName
*/
constructor(private val context: Context) {
private class DeepLinkDestination constructor(
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
index 4edde8c..db850b7 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavHostController.kt
@@ -69,7 +69,7 @@
* [NavHost].
* @throws IllegalStateException if you have not called
* [.setLifecycleOwner] before calling this method.
- * @see .setLifecycleOwner
+ * @see NavHostController.setLifecycleOwner
*/
public final override fun setOnBackPressedDispatcher(dispatcher: OnBackPressedDispatcher) {
super.setOnBackPressedDispatcher(dispatcher)
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
index 55b453f..f929423 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
@@ -168,7 +168,7 @@
* @param navController The NavController whose navigation actions will be reflected
* in the title of the action bar.
* @param openableLayout The Openable layout that should be toggled from the home button
- * @see .setupActionBarWithNavController
+ * @see NavigationUI.setupActionBarWithNavController
*/
@JvmStatic
public fun setupActionBarWithNavController(
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
index c9634cb..775f65b 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/custom/PagedListSampleActivity.kt
@@ -20,13 +20,11 @@
import android.widget.Button
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Observer
import androidx.paging.LoadState
import androidx.paging.LoadState.Error
-import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadState.Loading
+import androidx.paging.LoadState.NotLoading
import androidx.paging.LoadType
-import androidx.paging.PagedList
import androidx.paging.integration.testapp.R
import androidx.paging.integration.testapp.v3.StateItemAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -48,10 +46,9 @@
)
@Suppress("DEPRECATION")
- viewModel.livePagedList.observe(
- this,
- Observer<PagedList<Item>> { pagingAdapter.submitList(it) }
- )
+ viewModel.livePagedList.observe(this) { pagedList ->
+ pagingAdapter.submitList(pagedList)
+ }
setupLoadStateButtons(viewModel, pagingAdapter)
@@ -79,7 +76,7 @@
button.text = if (state.endOfPaginationReached) "Refresh" else "Done"
button.isEnabled = state.endOfPaginationReached
}
- is Loading -> {
+ Loading -> {
button.text = "Loading"
button.isEnabled = false
}
diff --git a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
index b9b3d3d..e07551d 100644
--- a/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
+++ b/paging/runtime/src/androidTest/java/androidx/paging/LivePagedListBuilderTest.kt
@@ -19,7 +19,6 @@
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.Observer
import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.paging.LoadState.Error
import androidx.paging.LoadState.Loading
@@ -42,6 +41,7 @@
import org.junit.runner.RunWith
@SmallTest
+@Suppress("DEPRECATION")
@RunWith(AndroidJUnit4::class)
class LivePagedListBuilderTest {
private val backgroundExecutor = TestExecutor()
@@ -78,7 +78,7 @@
ArchTaskExecutor.getInstance().setDelegate(null)
}
- class MockDataSourceFactory {
+ class MockPagingSourceFactory {
fun create(): PagingSource<Int, String> {
return MockPagingSource()
}
@@ -136,8 +136,7 @@
// represent the common case when writing tests.
ArchTaskExecutor.getInstance().setDelegate(null)
- @Suppress("DEPRECATION")
- LivePagedListBuilder(MockDataSourceFactory()::create, 2)
+ LivePagedListBuilder(MockPagingSourceFactory()::create, 2)
.build()
}
@@ -145,21 +144,15 @@
fun executorBehavior() {
// specify a background dispatcher via builder, and verify it gets used for all loads,
// overriding default IO dispatcher
- @Suppress("DEPRECATION")
- val livePagedList = LivePagedListBuilder(MockDataSourceFactory()::create, 2)
+ val livePagedList = LivePagedListBuilder(MockPagingSourceFactory()::create, 2)
.setFetchExecutor(backgroundExecutor)
.build()
- @Suppress("DEPRECATION")
val pagedListHolder: Array<PagedList<String>?> = arrayOfNulls(1)
- @Suppress("DEPRECATION")
- livePagedList.observe(
- lifecycleOwner,
- Observer<PagedList<String>> { newList ->
- pagedListHolder[0] = newList
- }
- )
+ livePagedList.observe(lifecycleOwner) { newList ->
+ pagedListHolder[0] = newList
+ }
// initially, immediately get passed empty initial list
assertNotNull(pagedListHolder[0])
@@ -181,24 +174,18 @@
@Test
fun failedLoad() {
- val factory = MockDataSourceFactory()
+ val factory = MockPagingSourceFactory()
factory.enqueueError()
- @Suppress("DEPRECATION")
val livePagedList = LivePagedListBuilder(factory::create, 2)
.setFetchExecutor(backgroundExecutor)
.build()
- @Suppress("DEPRECATION")
val pagedListHolder: Array<PagedList<String>?> = arrayOfNulls(1)
- @Suppress("DEPRECATION")
- livePagedList.observe(
- lifecycleOwner,
- Observer<PagedList<String>> { newList ->
- pagedListHolder[0] = newList
- }
- )
+ livePagedList.observe(lifecycleOwner) { newList ->
+ pagedListHolder[0] = newList
+ }
val loadStates = mutableListOf<LoadStateEvent>()
diff --git a/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
index 26e7cd5..907a45a 100644
--- a/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
+++ b/paging/runtime/src/main/java/androidx/paging/LivePagedListBuilder.kt
@@ -274,8 +274,8 @@
/**
* Constructs the `LiveData<PagedList>`.
*
- * No work (such as loading) is done immediately, the creation of the first PagedList is is
- * deferred until the LiveData is observed.
+ * No work (such as loading) is done immediately, the creation of the first [PagedList] is
+ * deferred until the [LiveData] is observed.
*
* @return The [LiveData] of [PagedList]s
*/
diff --git a/resourceinspection/resourceinspection-processor/build.gradle b/resourceinspection/resourceinspection-processor/build.gradle
index 4b3a931..7e881ef9 100644
--- a/resourceinspection/resourceinspection-processor/build.gradle
+++ b/resourceinspection/resourceinspection-processor/build.gradle
@@ -31,7 +31,8 @@
implementation(project(":resourceinspection:resourceinspection-annotation"))
implementation(project(":annotation:annotation"))
- implementation(AUTO_COMMON)
+ // TODO(183520738): Upgrade auto-common dependency to newest version 0.11.
+ implementation("com.google.auto:auto-common:0.10")
implementation(AUTO_SERVICE_ANNOTATIONS)
implementation(GRADLE_INCAP_HELPER)
implementation(GUAVA)
diff --git a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
index 5bc1d03..4263bda 100644
--- a/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
+++ b/room/compiler-processing-testing/src/main/java/androidx/room/compiler/processing/util/KotlinCompilationUtil.kt
@@ -17,6 +17,7 @@
package androidx.room.compiler.processing.util
import com.tschuchort.compiletesting.KotlinCompilation
+import org.jetbrains.kotlin.config.JvmTarget
import java.io.File
import java.io.OutputStream
import java.net.URLClassLoader
@@ -43,7 +44,7 @@
// workaround for https://github.com/tschuchortdev/kotlin-compile-testing/issues/105
compilation.kotlincArguments += "-Xjava-source-roots=${javaSrcRoot.absolutePath}"
compilation.jvmDefault = "enable"
- compilation.jvmTarget = "1.8"
+ compilation.jvmTarget = JvmTarget.JVM_1_8.description
compilation.inheritClassPath = false
compilation.verbose = false
compilation.classpaths = Classpaths.inheritedClasspath + classpaths
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt
new file mode 100644
index 0000000..815df3f
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/InternalXAnnotated.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.room.compiler.processing
+
+import kotlin.reflect.KClass
+
+/**
+ * Internal API for [XAnnotated] that handles repeated annotations.
+ */
+internal interface InternalXAnnotated : XAnnotated {
+ /**
+ * Repeated annotations show up differently between source and .class files.
+ *
+ * To avoid that inconsistency, [XAnnotated] only provides [XAnnotated.getAnnotations] and in
+ * this internal wrapper, we handle that inconsistency by finding the container class and
+ * asking implementers to implement the 2 arg version instead.
+ *
+ * see: https://github.com/google/ksp/issues/356
+ * see: https://github.com/google/ksp/issues/358
+ * see: https://youtrack.jetbrains.com/issue/KT-12794
+ *
+ * @param annotation The annotation to query
+ * @param containerAnnotation The container annotation of the [annotation] if it is a repeatable
+ * annotation.
+ *
+ * @see hasAnnotation
+ */
+ fun <T : Annotation> getAnnotations(
+ annotation: KClass<T>,
+ containerAnnotation: KClass<out Annotation>? = annotation.containerAnnotation
+ ): List<XAnnotationBox<T>>
+
+ override fun <T : Annotation> getAnnotations(annotation: KClass<T>) = getAnnotations(
+ annotation = annotation,
+ containerAnnotation = annotation.containerAnnotation
+ )
+
+ override fun hasAnnotation(annotation: KClass<out Annotation>) = hasAnnotation(
+ annotation = annotation,
+ containerAnnotation = annotation.containerAnnotation
+ )
+
+ /**
+ * Returns `true` if this element is annotated with the given [annotation].
+ *
+ * Note that this method should check for both [annotation] and [containerAnnotation] to
+ * support repeated annotations.
+ *
+ * @param annotation The annotation to query
+ * @param containerAnnotation The container annotation of the [annotation] if it is a repeatable
+ * annotation.
+ *
+ * @see [toAnnotationBox]
+ * @see [hasAnyOf]
+ */
+ fun hasAnnotation(
+ annotation: KClass<out Annotation>,
+ containerAnnotation: KClass<out Annotation>? = annotation.containerAnnotation
+ ): Boolean
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
similarity index 61%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
index e022dc3..02207f2 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/KClassExt.kt
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.room.compiler.processing
-import androidx.car.app.IOnDoneCallback;
+import kotlin.reflect.KClass
-/** @hide */
-oneway interface IOnInputCompletedListener {
- void onInputCompleted(String value, IOnDoneCallback callback) = 1;
-}
+private typealias JavaRepeatable = java.lang.annotation.Repeatable
+
+/**
+ * Returns the container annotation if `this` is a Repeatable annotation.
+ */
+internal val <T : Annotation> KClass<T>.containerAnnotation: KClass<out Annotation>?
+ get() = this.java.getAnnotation(JavaRepeatable::class.java)?.value
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
index bc43682..de23361 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XAnnotated.kt
@@ -23,13 +23,30 @@
*/
interface XAnnotated {
/**
- * If the current element has an annotation with the given [annotation] class, a boxed instance
- * of it will be returned where fields can be read. Otherwise, `null` value is returned.
+ * Gets the list of annotations with the given type.
+ *
+ * For repeated annotations declared in Java code, please use the repeated annotation type,
+ * not the container. Calling this method with a container annotation will have inconsistent
+ * behaviour between Java AP and KSP.
*
* @see [hasAnnotation]
* @see [hasAnnotationWithPackage]
*/
- fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>?
+ fun <T : Annotation> getAnnotations(
+ annotation: KClass<T>
+ ): List<XAnnotationBox<T>>
+
+ /**
+ * Returns `true` if this element is annotated with the given [annotation].
+ *
+ * For repeated annotations declared in Java code, please use the repeated annotation type,
+ * not the container. Calling this method with a container annotation will have inconsistent
+ * behaviour between Java AP and KSP.
+ * @see [hasAnyOf]
+ */
+ fun hasAnnotation(
+ annotation: KClass<out Annotation>
+ ): Boolean
/**
* Returns `true` if this element has an annotation that is declared in the given package.
@@ -38,15 +55,26 @@
fun hasAnnotationWithPackage(pkg: String): Boolean
/**
- * Returns `true` if this element is annotated with the given [annotation].
- *
- * @see [toAnnotationBox]
- * @see [hasAnyOf]
- */
- fun hasAnnotation(annotation: KClass<out Annotation>): Boolean
-
- /**
* Returns `true` if this element has one of the [annotations].
*/
fun hasAnyOf(vararg annotations: KClass<out Annotation>) = annotations.any(this::hasAnnotation)
+
+ @Deprecated(
+ replaceWith = ReplaceWith("getAnnotation(annotation)"),
+ message = "Use getAnnotation(not repeatable) or getAnnotations (repeatable)"
+ )
+ fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? =
+ getAnnotation(annotation)
+
+ /**
+ * If the current element has an annotation with the given [annotation] class, a boxed instance
+ * of it will be returned where fields can be read. Otherwise, `null` value is returned.
+ *
+ * @see [hasAnnotation]
+ * @see [getAnnotations]
+ * @see [hasAnnotationWithPackage]
+ */
+ fun <T : Annotation> getAnnotation(annotation: KClass<T>): XAnnotationBox<T>? {
+ return getAnnotations(annotation).firstOrNull()
+ }
}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
index 7063b75..725faa16 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingStep.kt
@@ -22,13 +22,12 @@
import androidx.room.compiler.processing.ksp.KspTypeElement
import com.google.auto.common.BasicAnnotationProcessor
import com.google.auto.common.MoreElements
-import com.google.common.collect.SetMultimap
+import com.google.common.collect.ImmutableSetMultimap
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Element
import javax.tools.Diagnostic
-import kotlin.reflect.KClass
/**
* Specialized processing step which only supports annotations on TypeElements.
@@ -47,13 +46,13 @@
*/
fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement>
/**
- * The set of annotations processed by this step.
+ * The set of annotation qualified names processed by this step.
*/
- fun annotations(): Set<KClass<out Annotation>>
+ fun annotations(): Set<String>
/**
* Wraps current [XProcessingStep] into an Auto Common
@@ -61,7 +60,7 @@
*/
fun asAutoCommonProcessor(
env: ProcessingEnvironment
- ): BasicAnnotationProcessor.ProcessingStep {
+ ): BasicAnnotationProcessor.Step {
return JavacProcessingStepDelegate(
env = env,
delegate = this
@@ -72,7 +71,7 @@
check(env is KspProcessingEnv)
val args = annotations().associateWith { annotation ->
val elements = env.resolver.getSymbolsWithAnnotation(
- annotation.java.canonicalName
+ annotation
).filterIsInstance<KSClassDeclaration>()
.map {
env.requireTypeElement(it.qualifiedName!!.asString())
@@ -84,15 +83,17 @@
}
}
-@Suppress("UnstableApiUsage")
internal class JavacProcessingStepDelegate(
val env: ProcessingEnvironment,
val delegate: XProcessingStep
-) : BasicAnnotationProcessor.ProcessingStep {
+) : BasicAnnotationProcessor.Step {
+ override fun annotations(): Set<String> = delegate.annotations()
+
+ @Suppress("UnstableApiUsage")
override fun process(
- elementsByAnnotation: SetMultimap<Class<out Annotation>, Element>
+ elementsByAnnotation: ImmutableSetMultimap<String, Element>
): Set<Element> {
- val converted = mutableMapOf<KClass<out Annotation>, List<XTypeElement>>()
+ val converted = mutableMapOf<String, List<XTypeElement>>()
// create a new x processing environment for each step to ensure it can freely cache
// whatever it wants and we don't keep elements references across rounds.
val xEnv = JavacProcessingEnv(env)
@@ -109,17 +110,11 @@
null
}
}
- converted[annotation.kotlin] = elements
+ converted[annotation] = elements
}
val result = delegate.process(xEnv, converted)
return result.map {
(it as JavacElement).element
}.toSet()
}
-
- override fun annotations(): Set<Class<out Annotation>> {
- return delegate.annotations().mapTo(mutableSetOf()) {
- it.java
- }
- }
}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
index 0f41811..e7b19ef 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XRoundEnv.kt
@@ -36,9 +36,9 @@
val rootElements: Set<XElement>
/**
- * Returns the set of [XElement]s that are annotated with the given [klass].
+ * Returns the set of [XElement]s that are annotated with the given [annotationQualifiedName].
*/
- fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement>
+ fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement>
companion object {
/**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
index d12eb4d..7397551 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacElement.kt
@@ -16,10 +16,12 @@
package androidx.room.compiler.processing.javac
+import androidx.room.compiler.processing.InternalXAnnotated
import androidx.room.compiler.processing.XAnnotationBox
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XEquality
import com.google.auto.common.MoreElements
+import com.google.auto.common.MoreElements.isAnnotationPresent
import java.util.Locale
import javax.lang.model.element.Element
import kotlin.reflect.KClass
@@ -28,16 +30,34 @@
internal abstract class JavacElement(
protected val env: JavacProcessingEnv,
open val element: Element
-) : XElement, XEquality {
- override fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? {
- return MoreElements
- .getAnnotationMirror(element, annotation.java)
- .orNull()
- ?.box(env, annotation.java)
+) : XElement, XEquality, InternalXAnnotated {
+ override fun <T : Annotation> getAnnotations(
+ annotation: KClass<T>,
+ containerAnnotation: KClass<out Annotation>?
+ ): List<XAnnotationBox<T>> {
+ return if (containerAnnotation == null) {
+ MoreElements
+ .getAnnotationMirror(element, annotation.java)
+ .orNull()
+ ?.box(env, annotation.java)
+ ?.let {
+ listOf(it)
+ }
+ } else {
+ val container = MoreElements
+ .getAnnotationMirror(element, containerAnnotation.java)
+ .orNull()
+ ?.box(env, containerAnnotation.java)
+ container?.getAsAnnotationBoxArray<T>("value")?.toList()
+ } ?: emptyList()
}
- override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
- return MoreElements.isAnnotationPresent(element, annotation.java)
+ override fun hasAnnotation(
+ annotation: KClass<out Annotation>,
+ containerAnnotation: KClass<out Annotation>?
+ ): Boolean {
+ return isAnnotationPresent(element, annotation.java) ||
+ (containerAnnotation != null && isAnnotationPresent(element, containerAnnotation.java))
}
override fun toString(): String {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
index 672a539..d878f1d 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacRoundEnv.kt
@@ -37,8 +37,10 @@
}
// TODO this is only for tests but we may need to support more types of elements
- override fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement> {
- val result = delegate.getElementsAnnotatedWith(klass)
+ override fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement> {
+ val element = env.elementUtils.getTypeElement(annotationQualifiedName)
+ ?: error("Cannot find TypeElement: $annotationQualifiedName")
+ val result = delegate.getElementsAnnotatedWith(element)
return result.filter {
MoreElements.isType(it)
}.map {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
index 0e6a7aa..0cf6f88 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotated.kt
@@ -16,6 +16,7 @@
package androidx.room.compiler.processing.ksp
+import androidx.room.compiler.processing.InternalXAnnotated
import androidx.room.compiler.processing.XAnnotated
import androidx.room.compiler.processing.XAnnotationBox
import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget
@@ -25,20 +26,40 @@
internal sealed class KspAnnotated(
val env: KspProcessingEnv
-) : XAnnotated {
+) : InternalXAnnotated {
abstract fun annotations(): Sequence<KSAnnotation>
- override fun <T : Annotation> toAnnotationBox(annotation: KClass<T>): XAnnotationBox<T>? {
- return annotations().firstOrNull {
+ private fun <T : Annotation> findAnnotations(annotation: KClass<T>): Sequence<KSAnnotation> {
+ return annotations().filter {
val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
qName == annotation.qualifiedName
- }?.let {
+ }
+ }
+
+ override fun <T : Annotation> getAnnotations(
+ annotation: KClass<T>,
+ containerAnnotation: KClass<out Annotation>?
+ ): List<XAnnotationBox<T>> {
+ // we'll try both because it can be the container or the annotation itself.
+ // try container first
+ if (containerAnnotation != null) {
+ // if container also repeats, this won't work but we don't have that use case
+ findAnnotations(containerAnnotation).firstOrNull()?.let {
+ return KspAnnotationBox(
+ env = env,
+ annotation = it,
+ annotationClass = containerAnnotation.java,
+ ).getAsAnnotationBoxArray<T>("value").toList()
+ }
+ }
+ // didn't find anything with the container, try the annotation class
+ return findAnnotations(annotation).map {
KspAnnotationBox(
env = env,
annotationClass = annotation.java,
annotation = it
)
- }
+ }.toList()
}
override fun hasAnnotationWithPackage(pkg: String): Boolean {
@@ -47,10 +68,14 @@
}
}
- override fun hasAnnotation(annotation: KClass<out Annotation>): Boolean {
+ override fun hasAnnotation(
+ annotation: KClass<out Annotation>,
+ containerAnnotation: KClass<out Annotation>?
+ ): Boolean {
return annotations().any {
val qName = it.annotationType.resolve().declaration.qualifiedName?.asString()
- qName == annotation.qualifiedName
+ qName == annotation.qualifiedName ||
+ (containerAnnotation != null && qName == containerAnnotation.qualifiedName)
}
}
@@ -143,4 +168,4 @@
} ?: NotAnnotated(env)
}
}
-}
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
index 864b472..8162e8c 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspRoundEnv.kt
@@ -29,9 +29,9 @@
override val rootElements: Set<XElement>
get() = TODO("not supported")
- override fun getTypeElementsAnnotatedWith(klass: Class<out Annotation>): Set<XTypeElement> {
+ override fun getTypeElementsAnnotatedWith(annotationQualifiedName: String): Set<XTypeElement> {
return env.resolver.getSymbolsWithAnnotation(
- klass.canonicalName
+ annotationQualifiedName
).filterIsInstance<KSClassDeclaration>()
.map {
env.wrapClassDeclaration(it)
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
index 625454e..3f67ea3 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/JavacTestProcessorTest.kt
@@ -42,7 +42,7 @@
override fun doProcess(annotations: Set<XTypeElement>, roundEnv: XRoundEnv): Boolean {
invoked.set(true)
val annotatedElements = roundEnv.getTypeElementsAnnotatedWith(
- OtherAnnotation::class.java
+ OtherAnnotation::class.qualifiedName!!
)
val targetElement = xProcessingEnv.requireTypeElement("foo.bar.Baz")
assertThat(
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
index f8bf777..06a3390 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationBoxTest.kt
@@ -22,6 +22,7 @@
import androidx.room.compiler.processing.testcode.JavaEnum
import androidx.room.compiler.processing.testcode.MainAnnotation
import androidx.room.compiler.processing.testcode.OtherAnnotation
+import androidx.room.compiler.processing.testcode.RepeatableJavaAnnotation
import androidx.room.compiler.processing.testcode.TestSuppressWarnings
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.XTestInvocation
@@ -95,7 +96,7 @@
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
- val annotationBox = element.toAnnotationBox(TestSuppressWarnings::class)
+ val annotationBox = element.getAnnotation(TestSuppressWarnings::class)
assertThat(annotationBox).isNotNull()
assertThat(
annotationBox!!.value.value
@@ -134,7 +135,7 @@
listOf(mySource)
) {
val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
- element.toAnnotationBox(MainAnnotation::class)!!.let { annotation ->
+ element.getAnnotation(MainAnnotation::class)!!.let { annotation ->
assertThat(
annotation.getAsTypeList("typeList")
).containsExactly(
@@ -177,7 +178,7 @@
sources = listOf(source)
) {
val element = it.processingEnv.requireTypeElement("Subject")
- val annotationBox = element.toAnnotationBox(TestSuppressWarnings::class)
+ val annotationBox = element.getAnnotation(TestSuppressWarnings::class)
assertThat(annotationBox).isNotNull()
assertThat(
annotationBox!!.value.value
@@ -217,7 +218,7 @@
listOf(mySource)
) { invocation ->
val element = invocation.processingEnv.requireTypeElement("Subject")
- element.toAnnotationBox(MainAnnotation::class)!!.let { annotation ->
+ element.getAnnotation(MainAnnotation::class)!!.let { annotation ->
assertThat(
annotation.getAsTypeList("typeList").map {
it.typeName
@@ -267,7 +268,7 @@
sources = listOf(src)
) { invocation ->
val subject = invocation.processingEnv.requireTypeElement("Subject")
- val annotationValue = subject.toAnnotationBox(
+ val annotationValue = subject.getAnnotation(
JavaAnnotationWithTypeReferences::class
)?.getAsTypeList("value")
assertThat(annotationValue?.map { it.typeName }).containsExactly(
@@ -423,7 +424,7 @@
invocation.processingEnv.requireTypeElement(it)
}.forEach { typeElement ->
val annotation =
- typeElement.toAnnotationBox(JavaAnnotationWithDefaults::class)
+ typeElement.getAnnotation(JavaAnnotationWithDefaults::class)
checkNotNull(annotation)
assertThat(annotation.value.intVal).isEqualTo(3)
assertThat(annotation.value.intArrayVal).isEqualTo(intArrayOf(1, 3, 5))
@@ -499,7 +500,7 @@
arrayOf("JavaSubject", "KotlinSubject").map {
invocation.processingEnv.requireTypeElement(it)
}.forEach { subject ->
- val annotation = subject.getField("annotated1").toAnnotationBox(
+ val annotation = subject.getField("annotated1").getAnnotation(
JavaAnnotationWithPrimitiveArray::class
)
assertThat(
@@ -511,9 +512,65 @@
}
}
+ @Test
+ fun javaRepeatableAnnotation() {
+ val javaSrc = Source.java(
+ "JavaSubject",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName};
+ @RepeatableJavaAnnotation("x")
+ @RepeatableJavaAnnotation("y")
+ @RepeatableJavaAnnotation("z")
+ public class JavaSubject {}
+ """.trimIndent()
+ )
+ val kotlinSrc = Source.kotlin(
+ "KotlinSubject.kt",
+ """
+ import ${RepeatableJavaAnnotation::class.qualifiedName}
+ // TODO update when https://youtrack.jetbrains.com/issue/KT-12794 is fixed.
+ // right now, kotlin does not support repeatable annotations.
+ @RepeatableJavaAnnotation.List(
+ RepeatableJavaAnnotation("x"),
+ RepeatableJavaAnnotation("y"),
+ RepeatableJavaAnnotation("z")
+ )
+ public class KotlinSubject
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(javaSrc, kotlinSrc)
+ ) { invocation ->
+ listOf("JavaSubject", "KotlinSubject")
+ .map(invocation.processingEnv::requireTypeElement)
+ .forEach { subject ->
+ if (invocation.isKsp && preCompiled) {
+ // TODO remove once https://github.com/google/ksp/issues/356 is fixed
+ // KSP cannot read array of annotation values in compiled code
+ } else {
+ val annotations = subject.getAnnotations(
+ RepeatableJavaAnnotation::class
+ )
+ assertThat(
+ subject.hasAnnotation(
+ RepeatableJavaAnnotation::class
+ )
+ ).isTrue()
+ val values = annotations
+ .map {
+ it.value.value
+ }
+ assertWithMessage(subject.qualifiedName)
+ .that(values)
+ .containsExactly("x", "y", "z")
+ }
+ }
+ }
+ }
+
// helper function to read what we need
private fun XAnnotated.getSuppressValues(): Array<String>? {
- return this.toAnnotationBox(TestSuppressWarnings::class)?.value?.value
+ return this.getAnnotation(TestSuppressWarnings::class)?.value?.value
}
private fun XAnnotated.assertHasSuppressWithValue(vararg expected: String) {
@@ -541,7 +598,7 @@
}
private fun XAnnotated.getOtherAnnotationValue(): String? {
- return this.toAnnotationBox(OtherAnnotation::class)?.value?.value
+ return this.getAnnotation(OtherAnnotation::class)?.value?.value
}
companion object {
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
index 0eacab4..ac75488 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XProcessingStepTest.kt
@@ -54,23 +54,26 @@
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement> {
- elementsByAnnotation[OtherAnnotation::class]?.forEach {
+ elementsByAnnotation[OtherAnnotation::class.qualifiedName]?.forEach {
annotatedElements[OtherAnnotation::class] = it.qualifiedName
}
- elementsByAnnotation[MainAnnotation::class]?.forEach {
+ elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
annotatedElements[MainAnnotation::class] = it.qualifiedName
}
return emptySet()
}
- override fun annotations(): Set<KClass<out Annotation>> {
- return setOf(OtherAnnotation::class, MainAnnotation::class)
+ override fun annotations(): Set<String> {
+ return setOf(
+ OtherAnnotation::class.qualifiedName!!,
+ MainAnnotation::class.qualifiedName!!
+ )
}
}
val mainProcessor = object : BasicAnnotationProcessor() {
- override fun initSteps(): Iterable<ProcessingStep> {
+ override fun steps(): Iterable<Step> {
return listOf(
processingStep.asAutoCommonProcessor(processingEnv)
)
@@ -123,11 +126,11 @@
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement> {
// for each element annotated with Main annotation, create a class with Other
// annotation to trigger another round
- elementsByAnnotation[MainAnnotation::class]?.forEach {
+ elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
@@ -140,14 +143,17 @@
.build()
.writeTo(env.filer)
}
- elementsByAnnotation[OtherAnnotation::class]?.forEach {
+ elementsByAnnotation[OtherAnnotation::class.qualifiedName]?.forEach {
otherAnnotatedElements.add(it.type.typeName)
}
return emptySet()
}
- override fun annotations(): Set<KClass<out Annotation>> {
- return setOf(OtherAnnotation::class, MainAnnotation::class)
+ override fun annotations(): Set<String> {
+ return setOf(
+ OtherAnnotation::class.qualifiedName!!,
+ MainAnnotation::class.qualifiedName!!
+ )
}
}
val main = JavaFileObjects.forSourceString(
@@ -171,16 +177,16 @@
listOf(main)
).processedWith(
object : BasicAnnotationProcessor() {
- override fun initSteps(): Iterable<ProcessingStep> {
+ override fun steps(): Iterable<Step> {
return listOf(
processingStep.asAutoCommonProcessor(processingEnv)
)
}
- override fun getSupportedOptions(): MutableSet<String> {
- return mutableSetOf(
- MainAnnotation::class.java.canonicalName,
- OtherAnnotation::class.java.canonicalName
+ override fun getSupportedOptions(): Set<String> {
+ return setOf(
+ MainAnnotation::class.qualifiedName!!,
+ OtherAnnotation::class.qualifiedName!!
)
}
}
@@ -198,14 +204,14 @@
var roundCounter = 0
override fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement> {
elementPerRound[roundCounter++] = listOf(
env.requireTypeElement("foo.bar.Main"),
env.requireTypeElement("foo.bar.Main")
)
// trigger another round
- elementsByAnnotation[MainAnnotation::class]?.forEach {
+ elementsByAnnotation[MainAnnotation::class.qualifiedName]?.forEach {
val className = ClassName.get(it.packageName, "${it.name}_Impl")
val spec = TypeSpec.classBuilder(className)
.addAnnotation(
@@ -221,8 +227,11 @@
return emptySet()
}
- override fun annotations(): Set<KClass<out Annotation>> {
- return setOf(OtherAnnotation::class, MainAnnotation::class)
+ override fun annotations(): Set<String> {
+ return setOf(
+ OtherAnnotation::class.qualifiedName!!,
+ MainAnnotation::class.qualifiedName!!
+ )
}
}
val main = JavaFileObjects.forSourceString(
@@ -246,16 +255,16 @@
listOf(main)
).processedWith(
object : BasicAnnotationProcessor() {
- override fun initSteps(): Iterable<ProcessingStep> {
+ override fun steps(): Iterable<Step> {
return listOf(
processingStep.asAutoCommonProcessor(processingEnv)
)
}
- override fun getSupportedOptions(): MutableSet<String> {
- return mutableSetOf(
- MainAnnotation::class.java.canonicalName,
- OtherAnnotation::class.java.canonicalName
+ override fun getSupportedOptions(): Set<String> {
+ return setOf(
+ MainAnnotation::class.qualifiedName!!,
+ OtherAnnotation::class.qualifiedName!!
)
}
}
@@ -278,15 +287,15 @@
val processingStep = object : XProcessingStep {
override fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement> {
return elementsByAnnotation.values
.flatten()
.toSet()
}
- override fun annotations(): Set<KClass<out Annotation>> {
- return setOf(OtherAnnotation::class)
+ override fun annotations(): Set<String> {
+ return setOf(OtherAnnotation::class.qualifiedName!!)
}
}
var returned: List<KSAnnotated>? = null
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java
new file mode 100644
index 0000000..cc0ba38
--- /dev/null
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/testcode/RepeatableJavaAnnotation.java
@@ -0,0 +1,31 @@
+/*
+ * 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.room.compiler.processing.testcode;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Repeatable(RepeatableJavaAnnotation.List.class)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RepeatableJavaAnnotation {
+ String value();
+ @Retention(RetentionPolicy.RUNTIME)
+ @interface List {
+ RepeatableJavaAnnotation[] value();
+ }
+}
diff --git a/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index ede6dab..dd00786 100644
--- a/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -29,16 +29,15 @@
import androidx.room.writer.DaoWriter
import androidx.room.writer.DatabaseWriter
import java.io.File
-import kotlin.reflect.KClass
class DatabaseProcessingStep : XProcessingStep {
override fun process(
env: XProcessingEnv,
- elementsByAnnotation: Map<KClass<out Annotation>, List<XTypeElement>>
+ elementsByAnnotation: Map<String, List<XTypeElement>>
): Set<XTypeElement> {
val context = Context(env)
val rejectedElements = mutableSetOf<XTypeElement>()
- val databases = elementsByAnnotation[Database::class]
+ val databases = elementsByAnnotation[Database::class.qualifiedName]
?.mapNotNull {
try {
DatabaseProcessor(
@@ -100,8 +99,8 @@
return rejectedElements
}
- override fun annotations(): Set<KClass<out Annotation>> {
- return mutableSetOf(Database::class)
+ override fun annotations(): Set<String> {
+ return mutableSetOf(Database::class.qualifiedName!!)
}
/**
diff --git a/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
index 76a7a8a..ed1595c 100644
--- a/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/RoomProcessor.kt
@@ -38,7 +38,7 @@
/** Helper variable to avoid reporting the warning twice. */
private var jdkVersionHasBugReported = false
- override fun initSteps(): MutableIterable<ProcessingStep> {
+ override fun steps(): MutableIterable<Step> {
return mutableListOf(
DatabaseProcessingStep().asAutoCommonProcessor(processingEnv)
)
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
index 00268f2..d8818644 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/AutoMigrationProcessor.kt
@@ -26,6 +26,9 @@
import androidx.room.vo.AutoMigrationResult
import java.io.File
+// TODO: (b/183435544) Support downgrades in AutoMigrations.
+// TODO: (b/183007590) Use the callback in the AutoMigration annotation while end-to-end
+// testing, when column/table rename/deletes are supported
class AutoMigrationProcessor(
val context: Context,
val element: XTypeElement,
@@ -46,7 +49,8 @@
return null
}
- if (!context.processingEnv.requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
+ if (!context.processingEnv
+ .requireType(RoomTypeNames.AUTO_MIGRATION_CALLBACK)
.isAssignableFrom(element.type)
) {
context.logger.e(
@@ -56,7 +60,7 @@
return null
}
- val annotationBox = element.toAnnotationBox(AutoMigration::class)
+ val annotationBox = element.getAnnotation(AutoMigration::class)
if (annotationBox == null) {
context.logger.e(
element,
@@ -100,18 +104,18 @@
return null
}
+ // TODO: (b/183434667) Update the automigration result data object to handle complex
+ // schema changes' presence when writer code is introduced
return AutoMigrationResult(
element = element,
from = fromSchemaBundle.version,
to = toSchemaBundle.version,
- addedColumns = schemaDiff.addedColumn,
- addedTables = schemaDiff.addedTable
+ addedColumns = schemaDiff.addedColumns,
+ addedTables = schemaDiff.addedTables
)
}
- // TODO: File bug for not supporting downgrades.
- // TODO: (b/180389433) If the files don't exist the getSchemaFile() method should return
- // null and before calling process
+ // TODO: (b/180389433) Verify automigration schemas before calling the AutoMigrationProcessor
private fun getValidatedSchemaFile(version: Int): File? {
val schemaFile = File(
context.schemaOutFolder,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
index 4f284c2..aceb277 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -44,7 +44,7 @@
isError() || isVoid() || isNone()
fun findConverters(context: Context, element: XElement): ProcessResult {
- val annotation = element.toAnnotationBox(TypeConverters::class)
+ val annotation = element.getAnnotation(TypeConverters::class)
return annotation?.let {
val classes = it.getAsTypeList("value")
.mapTo(LinkedHashSet()) { it }
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
index 745bd1f..35ca51e 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseProcessor.kt
@@ -55,7 +55,7 @@
}
private fun doProcess(): Database {
- val dbAnnotation = element.toAnnotationBox(androidx.room.Database::class)!!
+ val dbAnnotation = element.getAnnotation(androidx.room.Database::class)!!
val entities = processEntities(dbAnnotation, element)
val viewsMap = processDatabaseViews(dbAnnotation)
@@ -125,7 +125,7 @@
element: XTypeElement,
latestDbSchema: DatabaseBundle
): List<AutoMigrationResult> {
- val dbAnnotation = element.toAnnotationBox(androidx.room.Database::class)!!
+ val dbAnnotation = element.getAnnotation(androidx.room.Database::class)!!
val autoMigrationList = dbAnnotation.getAsTypeList("autoMigrations")
context.checker.check(
autoMigrationList.isEmpty() || dbAnnotation.value.exportSchema,
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
index 0253959..67aecd0 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/DatabaseViewProcessor.kt
@@ -35,7 +35,7 @@
element, androidx.room.DatabaseView::class,
ProcessorErrors.VIEW_MUST_BE_ANNOTATED_WITH_DATABASE_VIEW
)
- val annotationBox = element.toAnnotationBox(androidx.room.DatabaseView::class)
+ val annotationBox = element.getAnnotation(androidx.room.DatabaseView::class)
val viewName: String = if (annotationBox != null) {
extractViewName(element, annotationBox.value)
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
index 9e08987..f436016 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/FieldProcessor.kt
@@ -37,7 +37,7 @@
fun process(): Field {
val member = element.asMemberOf(containing)
val type = member.typeName
- val columnInfo = element.toAnnotationBox(ColumnInfo::class)?.value
+ val columnInfo = element.getAnnotation(ColumnInfo::class)?.value
val name = element.name
val rawCName = if (columnInfo != null && columnInfo.name != ColumnInfo.INHERIT_FIELD_NAME) {
columnInfo.name
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
index eec6de9..7956c0d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/FtsTableEntityProcessor.kt
@@ -57,7 +57,7 @@
element, androidx.room.Entity::class,
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
)
- val entityAnnotation = element.toAnnotationBox(androidx.room.Entity::class)
+ val entityAnnotation = element.getAnnotation(androidx.room.Entity::class)
val tableName: String
if (entityAnnotation != null) {
tableName = extractTableName(element, entityAnnotation.value)
@@ -84,9 +84,9 @@
context.checker.check(pojo.relations.isEmpty(), element, ProcessorErrors.RELATION_IN_ENTITY)
val (ftsVersion, ftsOptions) = if (element.hasAnnotation(androidx.room.Fts3::class)) {
- FtsVersion.FTS3 to getFts3Options(element.toAnnotationBox(Fts3::class)!!)
+ FtsVersion.FTS3 to getFts3Options(element.getAnnotation(Fts3::class)!!)
} else {
- FtsVersion.FTS4 to getFts4Options(element.toAnnotationBox(Fts4::class)!!)
+ FtsVersion.FTS4 to getFts4Options(element.getAnnotation(Fts4::class)!!)
}
val shadowTableName = if (ftsOptions.contentEntity != null) {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index 8747d0d..05c820d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -140,7 +140,7 @@
}
val ignoredColumns =
- element.toAnnotationBox(androidx.room.Entity::class)?.value?.ignoredColumns?.toSet()
+ element.getAnnotation(androidx.room.Entity::class)?.value?.ignoredColumns?.toSet()
?: emptySet()
val fieldBindingErrors = mutableMapOf<Field, String>()
val unfilteredMyFields = allFields[null]
@@ -403,7 +403,7 @@
return null
}
- val fieldPrefix = variableElement.toAnnotationBox(Embedded::class)?.value?.prefix ?: ""
+ val fieldPrefix = variableElement.getAnnotation(Embedded::class)?.value?.prefix ?: ""
val inheritedPrefix = parent?.prefix ?: ""
val embeddedField = Field(
variableElement,
@@ -432,7 +432,7 @@
container: XType?,
relationElement: XFieldElement
): androidx.room.vo.Relation? {
- val annotation = relationElement.toAnnotationBox(Relation::class)!!
+ val annotation = relationElement.getAnnotation(Relation::class)!!
val parentField = myFields.firstOrNull {
it.columnName == annotation.value.parentColumn
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 05a3b6a..a62aac8 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -841,8 +841,9 @@
}
fun columnWithChangedSchemaFound(columnName: String): String {
- return "Encountered column '$columnName' with a changed FieldBundle schema. This change " +
- "is not currently supported by AutoMigration."
+ return "Encountered column '$columnName' with an unsupported schema change at the column " +
+ "level (e.g. affinity change). These changes are not yet " +
+ "supported by AutoMigration."
}
fun removedOrRenamedColumnFound(columnName: String): String {
@@ -850,6 +851,12 @@
"renamed. This change is not currently supported by AutoMigration."
}
+ fun tableWithComplexChangedSchemaFound(tableName: String): String {
+ return "Encountered table '$tableName' with an unsupported schema change at the table " +
+ "level (e.g. primary key, foreign key or index change). These changes are not yet " +
+ "supported by AutoMigration."
+ }
+
fun removedOrRenamedTableFound(tableName: String): String {
return "Table '$tableName' has been either removed or " +
"renamed. This change is not currently supported by AutoMigration."
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
index b495976..9bee7d3 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/QueryMethodProcessor.kt
@@ -48,7 +48,7 @@
* implemented as a sub procedure in [InternalQueryProcessor].
*/
fun process(): QueryMethod {
- val annotation = executableElement.toAnnotationBox(Query::class)?.value
+ val annotation = executableElement.getAnnotation(Query::class)?.value
context.checker.check(
annotation != null, executableElement,
ProcessorErrors.MISSING_QUERY_ANNOTATION
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
index 1ee04bb..4d2a30d 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/RawQueryMethodProcessor.kt
@@ -72,7 +72,7 @@
}
private fun processObservedTables(): Set<String> {
- val annotation = executableElement.toAnnotationBox(RawQuery::class)
+ val annotation = executableElement.getAnnotation(RawQuery::class)
return annotation?.getAsTypeList("observedEntities")
?.mapNotNull {
it.typeElement.also { typeElement ->
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 0bf456b..b6bddc9 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -39,7 +39,7 @@
private val delegate = MethodProcessorDelegate.createFor(context, containing, executableElement)
fun <T : Annotation> extractAnnotation(klass: KClass<T>, errorMsg: String): XAnnotationBox<T>? {
- val annotation = executableElement.toAnnotationBox(klass)
+ val annotation = executableElement.getAnnotation(klass)
context.checker.check(annotation != null, executableElement, errorMsg)
return annotation
}
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
index 390b8bd..7781560 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/SuppressWarningProcessor.kt
@@ -25,7 +25,7 @@
object SuppressWarningProcessor {
fun getSuppressedWarnings(element: XElement): Set<Warning> {
- val annotation = element.toAnnotationBox(SuppressWarnings::class)?.value
+ val annotation = element.getAnnotation(SuppressWarnings::class)?.value
return if (annotation == null) {
emptySet()
} else {
diff --git a/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt b/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
index efc5e84..4f18a69 100644
--- a/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/processor/TableEntityProcessor.kt
@@ -58,7 +58,7 @@
element, androidx.room.Entity::class,
ProcessorErrors.ENTITY_MUST_BE_ANNOTATED_WITH_ENTITY
)
- val annotationBox = element.toAnnotationBox(androidx.room.Entity::class)
+ val annotationBox = element.getAnnotation(androidx.room.Entity::class)
val tableName: String
val entityIndices: List<IndexInput>
val foreignKeyInputs: List<ForeignKeyInput>
@@ -232,7 +232,7 @@
context.logger.e(element, ProcessorErrors.FOREIGN_KEY_CANNOT_FIND_PARENT)
return@map null
}
- val parentAnnotation = parentElement.toAnnotationBox(androidx.room.Entity::class)
+ val parentAnnotation = parentElement.getAnnotation(androidx.room.Entity::class)
if (parentAnnotation == null) {
context.logger.e(
element,
@@ -328,7 +328,7 @@
*/
private fun collectPrimaryKeysFromPrimaryKeyAnnotations(fields: List<Field>): List<PrimaryKey> {
return fields.mapNotNull { field ->
- field.element.toAnnotationBox(androidx.room.PrimaryKey::class)?.let {
+ field.element.getAnnotation(androidx.room.PrimaryKey::class)?.let {
if (field.parent != null) {
// the field in the entity that contains this error.
val grandParentField = field.parent.mRootParent.field.element
@@ -359,7 +359,7 @@
typeElement: XTypeElement,
availableFields: List<Field>
): List<PrimaryKey> {
- val myPkeys = typeElement.toAnnotationBox(androidx.room.Entity::class)?.let {
+ val myPkeys = typeElement.getAnnotation(androidx.room.Entity::class)?.let {
val primaryKeyColumns = it.value.primaryKeys
if (primaryKeyColumns.isEmpty()) {
emptyList()
@@ -402,7 +402,7 @@
embeddedFields: List<EmbeddedField>
): List<PrimaryKey> {
return embeddedFields.mapNotNull { embeddedField ->
- embeddedField.field.element.toAnnotationBox(androidx.room.PrimaryKey::class)?.let {
+ embeddedField.field.element.getAnnotation(androidx.room.PrimaryKey::class)?.let {
context.checker.check(
!it.value.autoGenerate || embeddedField.pojo.fields.size == 1,
embeddedField.field.element,
@@ -494,7 +494,7 @@
// see if any embedded field is an entity with indices, if so, report a warning
pojo.embeddedFields.forEach { embedded ->
val embeddedElement = embedded.pojo.element
- embeddedElement.toAnnotationBox(androidx.room.Entity::class)?.let {
+ embeddedElement.getAnnotation(androidx.room.Entity::class)?.let {
val subIndices = extractIndices(it, "")
if (subIndices.isNotEmpty()) {
context.logger.w(
@@ -528,7 +528,7 @@
return emptyList()
}
val myIndices = parentTypeElement
- .toAnnotationBox(androidx.room.Entity::class)?.let { annotation ->
+ .getAnnotation(androidx.room.Entity::class)?.let { annotation ->
val indices = extractIndices(annotation, tableName = "super")
if (indices.isEmpty()) {
emptyList()
diff --git a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index a0a327d..53f1f231 100644
--- a/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -17,6 +17,9 @@
package androidx.room.util
import androidx.room.migration.bundle.DatabaseBundle
+import androidx.room.migration.bundle.EntityBundle
+import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.IndexBundle
import androidx.room.processor.ProcessorErrors
import androidx.room.vo.AutoMigrationResult
@@ -29,11 +32,12 @@
* Contains the added, changed and removed columns detected.
*/
data class SchemaDiffResult(
- val addedColumn: MutableList<AutoMigrationResult.AddedColumn>,
- val changedColumn: List<AutoMigrationResult.ChangedColumn>,
- val removedColumn: List<AutoMigrationResult.RemovedColumn>,
- val addedTable: List<AutoMigrationResult.AddedTable>,
- val removedTable: List<AutoMigrationResult.RemovedTable>
+ val addedColumns: List<AutoMigrationResult.AddedColumn>,
+ val changedColumns: List<AutoMigrationResult.ChangedColumn>,
+ val removedColumns: List<AutoMigrationResult.RemovedColumn>,
+ val addedTables: List<AutoMigrationResult.AddedTable>,
+ val complexChangedTables: List<AutoMigrationResult.ComplexChangedTable>,
+ val removedTables: List<AutoMigrationResult.RemovedTable>
)
/**
@@ -55,6 +59,7 @@
*/
fun diffSchemas(): SchemaDiffResult {
val addedTables = mutableListOf<AutoMigrationResult.AddedTable>()
+ val complexChangedTables = mutableListOf<AutoMigrationResult.ComplexChangedTable>()
val removedTables = mutableListOf<AutoMigrationResult.RemovedTable>()
val addedColumns = mutableListOf<AutoMigrationResult.AddedColumn>()
@@ -63,28 +68,32 @@
// Check going from the original version of the schema to the new version for changed and
// removed columns/tables
- fromSchemaBundle.entitiesByTableName.forEach { v1Table ->
- val v2Table = toSchemaBundle.entitiesByTableName[v1Table.key]
- if (v2Table == null) {
- removedTables.add(AutoMigrationResult.RemovedTable(v1Table.value))
+ fromSchemaBundle.entitiesByTableName.forEach { fromTable ->
+ val toTable = toSchemaBundle.entitiesByTableName[fromTable.key]
+ if (toTable == null) {
+ removedTables.add(AutoMigrationResult.RemovedTable(fromTable.value))
} else {
- val v1Columns = v1Table.value.fieldsByColumnName
- val v2Columns = v2Table.fieldsByColumnName
- v1Columns.entries.forEach { v1Column ->
- val match = v2Columns[v1Column.key]
- if (match != null && !match.isSchemaEqual(v1Column.value)) {
+ val complexChangedTable = tableContainsComplexChanges(fromTable.value, toTable)
+ if (complexChangedTable != null) {
+ complexChangedTables.add(complexChangedTable)
+ }
+ val fromColumns = fromTable.value.fieldsByColumnName
+ val toColumns = toTable.fieldsByColumnName
+ fromColumns.entries.forEach { fromColumn ->
+ val match = toColumns[fromColumn.key]
+ if (match != null && !match.isSchemaEqual(fromColumn.value)) {
+ // Any change in the field bundle schema of a column will be complex
changedColumns.add(
AutoMigrationResult.ChangedColumn(
- v1Table.key,
- v1Column.value,
- match
+ fromTable.key,
+ fromColumn.value
)
)
} else if (match == null) {
removedColumns.add(
AutoMigrationResult.RemovedColumn(
- v1Table.key,
- v1Column.value
+ fromTable.key,
+ fromColumn.value
)
)
}
@@ -94,25 +103,25 @@
// Check going from the new version of the schema to the original version for added
// tables/columns. Skip the columns with the same name as the previous loop would have
// processed them already.
- toSchemaBundle.entitiesByTableName.forEach { v2Table ->
- val v1Table = fromSchemaBundle.entitiesByTableName[v2Table.key]
- if (v1Table == null) {
- addedTables.add(AutoMigrationResult.AddedTable(v2Table.value))
+ toSchemaBundle.entitiesByTableName.forEach { toTable ->
+ val fromTable = fromSchemaBundle.entitiesByTableName[toTable.key]
+ if (fromTable == null) {
+ addedTables.add(AutoMigrationResult.AddedTable(toTable.value))
} else {
- val v2Columns = v2Table.value.fieldsByColumnName
- val v1Columns = v1Table.fieldsByColumnName
- v2Columns.entries.forEach { v2Column ->
- val match = v1Columns[v2Column.key]
+ val fromColumns = fromTable.fieldsByColumnName
+ val toColumns = toTable.value.fieldsByColumnName
+ toColumns.entries.forEach { toColumn ->
+ val match = fromColumns[toColumn.key]
if (match == null) {
- if (v2Column.value.isNonNull && v2Column.value.defaultValue == null) {
+ if (toColumn.value.isNonNull && toColumn.value.defaultValue == null) {
diffError(
- ProcessorErrors.newNotNullColumnMustHaveDefaultValue(v2Column.key)
+ ProcessorErrors.newNotNullColumnMustHaveDefaultValue(toColumn.key)
)
}
addedColumns.add(
AutoMigrationResult.AddedColumn(
- v2Table.key,
- v2Column.value
+ toTable.key,
+ toColumn.value
)
)
}
@@ -120,11 +129,13 @@
}
}
+ // TODO: (b/183007590) Remove the Processor Errors thrown when a complex change is
+ // encountered after AutoMigrationWriter supports generating the necessary migrations.
if (changedColumns.isNotEmpty()) {
changedColumns.forEach { changedColumn ->
diffError(
ProcessorErrors.columnWithChangedSchemaFound(
- changedColumn.originalFieldBundle.columnName
+ changedColumn.fieldBundle.columnName
)
)
}
@@ -140,6 +151,16 @@
}
}
+ if (complexChangedTables.isNotEmpty()) {
+ complexChangedTables.forEach { changedTable ->
+ diffError(
+ ProcessorErrors.tableWithComplexChangedSchemaFound(
+ changedTable.tableName
+ )
+ )
+ }
+ }
+
if (removedTables.isNotEmpty()) {
removedTables.forEach { removedTable ->
diffError(
@@ -151,15 +172,98 @@
}
return SchemaDiffResult(
- addedColumn = addedColumns,
- changedColumn = changedColumns,
- removedColumn = removedColumns,
- addedTable = addedTables,
- removedTable = removedTables
+ addedColumns = addedColumns,
+ changedColumns = changedColumns,
+ removedColumns = removedColumns,
+ addedTables = addedTables,
+ complexChangedTables = complexChangedTables,
+ removedTables = removedTables
)
}
+ /**
+ * Check for complex schema changes at a Table level and returns a ComplexTableChange
+ * including information on which table changes were found on, and whether foreign key or
+ * index related changes have occurred.
+ *
+ * @return null if complex schema change has not been found
+ */
+ // TODO: (b/181777611) Handle FTS tables
+ private fun tableContainsComplexChanges(
+ fromTable: EntityBundle,
+ toTable: EntityBundle
+ ): AutoMigrationResult.ComplexChangedTable? {
+ val foreignKeyChanged = !isForeignKeyBundlesListEqual(
+ fromTable.foreignKeys,
+ toTable.foreignKeys
+ )
+ val indexChanged = !isIndexBundlesListEqual(fromTable.indices, toTable.indices)
+ val primaryKeyChanged = !fromTable.primaryKey.isSchemaEqual(toTable.primaryKey)
+
+ if (primaryKeyChanged || foreignKeyChanged || indexChanged) {
+ return AutoMigrationResult.ComplexChangedTable(
+ tableName = toTable.tableName,
+ foreignKeyChanged = foreignKeyChanged,
+ indexChanged = indexChanged
+ )
+ }
+ return null
+ }
+
private fun diffError(errorMsg: String) {
throw DiffException(errorMsg)
}
+
+ /**
+ * Takes in two ForeignKeyBundle lists, attempts to find potential matches based on the columns
+ * of the Foreign Keys. Processes these potential matches by checking for schema equality.
+ *
+ * @return true if the two lists of foreign keys are equal
+ */
+ private fun isForeignKeyBundlesListEqual(
+ fromBundle: List<ForeignKeyBundle>,
+ toBundle: List<ForeignKeyBundle>
+ ): Boolean {
+ val set = fromBundle + toBundle
+ val matches = set.groupBy { it.columns }.entries
+
+ matches.forEach { (_, bundles) ->
+ if (bundles.size < 2) {
+ // A bundle was not matched at all, there must be a change between two versions
+ return false
+ }
+ val fromForeignKeyBundle = bundles[0]
+ val toForeignKeyBundle = bundles[1]
+ if (!fromForeignKeyBundle.isSchemaEqual(toForeignKeyBundle)) {
+ // A potential match for a bundle was found, but schemas did not match
+ return false
+ }
+ }
+ return true
+ }
+
+ /**
+ * Takes in two IndexBundle lists, attempts to find potential matches based on the names
+ * of the indexes. Processes these potential matches by checking for schema equality.
+ *
+ * @return true if the two lists of indexes are equal
+ */
+ private fun isIndexBundlesListEqual(
+ fromBundle: List<IndexBundle>,
+ toBundle: List<IndexBundle>
+ ): Boolean {
+ val set = fromBundle + toBundle
+ val matches = set.groupBy { it.name }.entries
+
+ matches.forEach { bundlesWithSameName ->
+ if (bundlesWithSameName.value.size < 2) {
+ // A bundle was not matched at all, there must be a change between two versions
+ return false
+ } else if (!bundlesWithSameName.value[0].isSchemaEqual(bundlesWithSameName.value[1])) {
+ // A potential match for a bundle was found, but schemas did not match
+ return false
+ }
+ }
+ return true
+ }
}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
index 91d4bd6..f6fcc1a 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/AutoMigrationResult.kt
@@ -49,8 +49,7 @@
*/
data class ChangedColumn(
val tableName: String,
- val originalFieldBundle: FieldBundle,
- val newFieldBundle: FieldBundle
+ val fieldBundle: FieldBundle
)
/**
@@ -69,6 +68,25 @@
data class AddedTable(val entityBundle: EntityBundle)
/**
+ * Stores the table name that contains a change in the primary key, foreign key(s) or index(es)
+ * in a newer version. Explicitly provides information on whether a foreign key change and/or
+ * an index change has occurred.
+ *
+ * As it is possible to have a table with only simple (non-complex) changes, which will be
+ * categorized as "AddedColumn" or "RemovedColumn" changes, all other
+ * changes at the table level are categorized as "complex" changes, using the category
+ * "ComplexChangedTable".
+ *
+ * At the column level, any change that is not a column add or a
+ * removal will be categorized as "ChangedColumn".
+ */
+ data class ComplexChangedTable(
+ val tableName: String,
+ val foreignKeyChanged: Boolean,
+ val indexChanged: Boolean
+ )
+
+ /**
* Stores the table that was present in the old version of a database but is not present in a
* new version of the same database, either because it was removed or renamed.
*
diff --git a/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
index 3922f26..b463a7b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/vo/Pojo.kt
@@ -42,11 +42,11 @@
* Might be via Embedded or Relation.
*/
fun accessedTableNames(): List<String> {
- val entityAnnotation = element.toAnnotationBox(androidx.room.Entity::class)
+ val entityAnnotation = element.getAnnotation(androidx.room.Entity::class)
return if (entityAnnotation != null) {
listOf(EntityProcessor.extractTableName(element, entityAnnotation.value))
} else {
- val viewAnnotation = element.toAnnotationBox(androidx.room.DatabaseView::class)
+ val viewAnnotation = element.getAnnotation(androidx.room.DatabaseView::class)
if (viewAnnotation != null) {
listOf(DatabaseViewProcessor.extractViewName(element, viewAnnotation.value))
} else {
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
index c442c6f..7e79cd0 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseEntityParserTest.kt
@@ -68,7 +68,7 @@
) { invocation ->
val entity = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Entity::class.java
+ androidx.room.Entity::class.qualifiedName!!
).first {
it.qualifiedName == "foo.bar.MyEntity"
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
index 8e83ba6..31f63f3 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/BaseFtsEntityParserTest.kt
@@ -99,9 +99,9 @@
)
.nextRunHandler { invocation ->
val fts3AnnotatedElements = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Fts3::class.java)
+ .getTypeElementsAnnotatedWith(Fts3::class.qualifiedName!!)
val fts4AnnotatedElements = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Fts4::class.java)
+ .getTypeElementsAnnotatedWith(Fts4::class.qualifiedName!!)
val entity = (fts3AnnotatedElements + fts4AnnotatedElements).first {
it.toString() == "foo.bar.MyEntity"
}
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
index d857001..081f84f 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DaoProcessorTest.kt
@@ -427,7 +427,7 @@
) { invocation: XTestInvocation ->
val dao = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Dao::class.java
+ androidx.room.Dao::class.qualifiedName!!
)
.first()
check(dao.isTypeElement())
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
index 6902a61..1e72450 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/DatabaseProcessorTest.kt
@@ -1164,7 +1164,7 @@
.nextRunHandler { invocation ->
val database = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Database::class.java
+ androidx.room.Database::class.qualifiedName!!
)
.first()
val processor = DatabaseProcessor(
@@ -1237,7 +1237,7 @@
.nextRunHandler { invocation ->
val entity = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Database::class.java
+ androidx.room.Database::class.qualifiedName!!
)
.first()
val parser = DatabaseProcessor(
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
index 4531a61..9a7fcb2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/FieldProcessorTest.kt
@@ -602,7 +602,7 @@
.forAnnotations(androidx.room.Entity::class)
.nextRunHandler { invocation ->
val (owner, fieldElement) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Entity::class.java)
+ .getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index b8ee82f..92b679c 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -89,7 +89,8 @@
"""
)
) { invocation ->
- val entity = invocation.roundEnv.getTypeElementsAnnotatedWith(Fts4::class.java)
+ val entity = invocation.roundEnv
+ .getTypeElementsAnnotatedWith(Fts4::class.qualifiedName!!)
.first { it.toString() == "foo.bar.MyEntity" }
FtsTableEntityProcessor(invocation.context, entity)
.process()
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
index 9f5d417..a0767a7 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/InsertionMethodProcessorTest.kt
@@ -863,7 +863,7 @@
.forAnnotations(Insert::class, Dao::class)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
index 3d03007..d086818 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ProjectionExpanderTest.kt
@@ -520,7 +520,7 @@
options = listOf("-Aroom.expandProjection=true")
) { invocation ->
val entities = invocation.roundEnv
- .getTypeElementsAnnotatedWith(androidx.room.Entity::class.java)
+ .getTypeElementsAnnotatedWith(androidx.room.Entity::class.qualifiedName!!)
.map { element ->
TableEntityProcessor(
invocation.context,
@@ -623,7 +623,7 @@
options = listOf("-Aroom.expandProjection=true")
) { invocation ->
val entities = invocation.roundEnv
- .getTypeElementsAnnotatedWith(androidx.room.Entity::class.java)
+ .getTypeElementsAnnotatedWith(androidx.room.Entity::class.qualifiedName!!)
.map { element ->
TableEntityProcessor(
invocation.context,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index 350c1ad..676bf07 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -1166,7 +1166,7 @@
)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
index e6b825b..6de8d49 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/RawQueryMethodProcessorTest.kt
@@ -343,7 +343,7 @@
)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
index 1810560..7c226d3 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/ShortcutMethodProcessorTest.kt
@@ -536,7 +536,7 @@
.forAnnotations(annotation, Dao::class)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
index 8fad90d..988a598 100644
--- a/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/processor/TransactionMethodProcessorTest.kt
@@ -251,7 +251,7 @@
.forAnnotations(Transaction::class, Dao::class)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
index cbb67cb..8976245 100644
--- a/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/solver/query/QueryWriterTest.kt
@@ -354,7 +354,7 @@
.forAnnotations(Query::class, Dao::class)
.nextRunHandler { invocation ->
val (owner, methods) = invocation.roundEnv
- .getTypeElementsAnnotatedWith(Dao::class.java)
+ .getTypeElementsAnnotatedWith(Dao::class.qualifiedName!!)
.map {
Pair(
it,
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt b/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
index 669f928..ddee174 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/XProcessingStepExt.kt
@@ -27,7 +27,7 @@
delegate: (XTestInvocation) -> Unit
): (XTestInvocation) -> Unit = { invocation ->
val elementsByAnnotation = annotations().associateWith {
- invocation.roundEnv.getTypeElementsAnnotatedWith(it.java).toList()
+ invocation.roundEnv.getTypeElementsAnnotatedWith(it).toList()
}
this.process(
env = invocation.processingEnv,
diff --git a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index d38af3f..d3fd8b2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -278,26 +278,26 @@
}
fun XTestInvocation.getViews(): List<androidx.room.vo.DatabaseView> {
- return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.java).map {
+ return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.qualifiedName!!).map {
DatabaseViewProcessor(context, it).process()
}
}
fun XTestInvocation.getEntities(): List<androidx.room.vo.Entity> {
- val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.java).map {
+ val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!).map {
TableEntityProcessor(context, it).process()
}
return entities
}
fun TestInvocation.getViews(): List<androidx.room.vo.DatabaseView> {
- return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.java).map {
+ return roundEnv.getTypeElementsAnnotatedWith(DatabaseView::class.qualifiedName!!).map {
DatabaseViewProcessor(context, it).process()
}
}
fun TestInvocation.getEntities(): List<androidx.room.vo.Entity> {
- val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.java).map {
+ val entities = roundEnv.getTypeElementsAnnotatedWith(Entity::class.qualifiedName!!).map {
TableEntityProcessor(context, it).process()
}
return entities
diff --git a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
index 0dd90dc..94ed63a 100644
--- a/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/util/SchemaDifferTest.kt
@@ -19,6 +19,8 @@
import androidx.room.migration.bundle.DatabaseBundle
import androidx.room.migration.bundle.EntityBundle
import androidx.room.migration.bundle.FieldBundle
+import androidx.room.migration.bundle.ForeignKeyBundle
+import androidx.room.migration.bundle.IndexBundle
import androidx.room.migration.bundle.PrimaryKeyBundle
import androidx.room.migration.bundle.SchemaBundle
import androidx.room.processor.ProcessorErrors
@@ -29,12 +31,57 @@
class SchemaDifferTest {
@Test
+ fun testPrimaryKeyChanged() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toChangeInPrimaryKey.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+ )
+ }
+ }
+
+ @Test
+ fun testForeignKeyFieldChanged() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toForeignKeyAdded.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+ )
+ }
+ }
+
+ @Test
+ fun testComplexChangeInvolvingIndex() {
+ try {
+ SchemaDiffer(
+ fromSchemaBundle = from.database,
+ toSchemaBundle = toIndexAdded.database
+ ).diffSchemas()
+ fail("DiffException should have been thrown.")
+ } catch (ex: DiffException) {
+ assertThat(ex.errorMessage).isEqualTo(
+ ProcessorErrors.tableWithComplexChangedSchemaFound("Song")
+ )
+ }
+ }
+
+ @Test
fun testColumnAddedWithColumnInfoDefaultValue() {
val schemaDiffResult = SchemaDiffer(
fromSchemaBundle = from.database,
toSchemaBundle = toColumnAddedWithColumnInfoDefaultValue.database
).diffSchemas()
- assertThat(schemaDiffResult.addedColumn[0].fieldBundle.columnName).isEqualTo("artistId")
+ assertThat(schemaDiffResult.addedColumns[0].fieldBundle.columnName).isEqualTo("artistId")
}
@Test
@@ -58,8 +105,8 @@
fromSchemaBundle = from.database,
toSchemaBundle = toTableAddedWithColumnInfoDefaultValue.database
).diffSchemas()
- assertThat(schemaDiffResult.addedTable[0].entityBundle.tableName).isEqualTo("Artist")
- assertThat(schemaDiffResult.addedTable[1].entityBundle.tableName).isEqualTo("Album")
+ assertThat(schemaDiffResult.addedTables[0].entityBundle.tableName).isEqualTo("Artist")
+ assertThat(schemaDiffResult.addedTables[1].entityBundle.tableName).isEqualTo("Album")
}
@Test
@@ -78,7 +125,7 @@
}
@Test
- fun testColumnAffinityChanged() {
+ fun testColumnFieldBundleChanged() {
try {
SchemaDiffer(
fromSchemaBundle = from.database,
@@ -496,10 +543,74 @@
)
)
- val toTableAddedWithNoDefaultValue = SchemaBundle(
- 1,
+ private val toForeignKeyAdded = SchemaBundle(
+ 2,
DatabaseBundle(
- 1,
+ 2,
+ "",
+ listOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, `artistId` " +
+ "INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`), FOREIGN KEY(`title`) " +
+ "REFERENCES `Song`(`artistId`) ON UPDATE NO ACTION ON DELETE NO " +
+ "ACTION DEFERRABLE INITIALLY DEFERRED))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "artistId",
+ "artistId",
+ "INTEGER",
+ true,
+ "0"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("id")
+ ),
+ emptyList(),
+ listOf(
+ ForeignKeyBundle(
+ "Song",
+ "onDelete",
+ "onUpdate",
+ listOf("title"),
+ listOf("artistId")
+ )
+ )
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ val toIndexAdded = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
"",
mutableListOf(
EntityBundle(
@@ -533,25 +644,62 @@
false,
mutableListOf("id")
),
- mutableListOf(),
- mutableListOf()
- ),
- EntityBundle(
- "Album",
- "CREATE TABLE IF NOT EXISTS `Album` (`id` INTEGER NOT NULL, `name` TEXT NOT " +
- "NULL, PRIMARY KEY(`id`))",
listOf(
- FieldBundle(
- "albumId",
- "albumId",
- "INTEGER",
+ IndexBundle(
+ "index1",
true,
- null
+ listOf("title"),
+ "CREATE UNIQUE INDEX IF NOT EXISTS `index1` ON `Song`" +
+ "(`title`)"
)
),
- PrimaryKeyBundle(true, listOf("id")),
- listOf(),
- listOf()
+ mutableListOf()
+ )
+ ),
+ mutableListOf(),
+ mutableListOf()
+ )
+ )
+
+ val toChangeInPrimaryKey = SchemaBundle(
+ 2,
+ DatabaseBundle(
+ 2,
+ "",
+ mutableListOf(
+ EntityBundle(
+ "Song",
+ "CREATE TABLE IF NOT EXISTS `Song` (`id` INTEGER NOT NULL, " +
+ "`title` TEXT NOT NULL, `length` INTEGER NOT NULL, PRIMARY KEY(`title`))",
+ listOf(
+ FieldBundle(
+ "id",
+ "id",
+ "INTEGER",
+ true,
+ "1"
+ ),
+ FieldBundle(
+ "title",
+ "title",
+ "TEXT",
+ true,
+ ""
+ ),
+ FieldBundle(
+ "length",
+ "length",
+ "INTEGER",
+ true,
+ "1"
+ )
+ ),
+ PrimaryKeyBundle(
+ false,
+ mutableListOf("title")
+ ),
+ mutableListOf(),
+ mutableListOf()
)
),
mutableListOf(),
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
index cda50d8..3882bd2 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/DaoWriterTest.kt
@@ -140,12 +140,12 @@
) { invocation ->
val dao = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Dao::class.java
+ androidx.room.Dao::class.qualifiedName!!
).firstOrNull()
if (dao != null) {
val db = invocation.roundEnv
.getTypeElementsAnnotatedWith(
- androidx.room.Database::class.java
+ androidx.room.Database::class.qualifiedName!!
).firstOrNull()
?: invocation.context.processingEnv
.requireTypeElement(RoomTypeNames.ROOM_DB)
diff --git a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
index fc1c911..8b1a973 100644
--- a/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
+++ b/room/compiler/src/test/kotlin/androidx/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -214,7 +214,7 @@
sources = sources + databaseCode
) { invocation ->
val db = invocation.roundEnv
- .getTypeElementsAnnotatedWith(androidx.room.Database::class.java)
+ .getTypeElementsAnnotatedWith(androidx.room.Database::class.qualifiedName!!)
.first()
handler(DatabaseProcessor(invocation.context, db).process(), invocation)
}
diff --git a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index a72e89b..f45d8e5 100644
--- a/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -23,7 +23,9 @@
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -102,29 +104,33 @@
tableNames: Array<String>,
callable: Callable<R>
): Flow<@JvmSuppressWildcards R> = flow {
- // Observer channel receives signals from the invalidation tracker to emit queries.
- val observerChannel = Channel<Unit>(Channel.CONFLATED)
- val observer = object : InvalidationTracker.Observer(tableNames) {
- override fun onInvalidated(tables: MutableSet<String>) {
- observerChannel.offer(Unit)
- }
- }
- observerChannel.offer(Unit) // Initial signal to perform first query.
- val flowContext = coroutineContext
- val queryContext = coroutineContext[TransactionElement]?.transactionDispatcher
- ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
- withContext(queryContext) {
- db.invalidationTracker.addObserver(observer)
- try {
- // Iterate until cancelled, transforming observer signals to query results to
- // be emitted to the flow.
- for (signal in observerChannel) {
- val result = callable.call()
- withContext(flowContext) { emit(result) }
+ coroutineScope {
+ // Observer channel receives signals from the invalidation tracker to emit queries.
+ val observerChannel = Channel<Unit>(Channel.CONFLATED)
+ val observer = object : InvalidationTracker.Observer(tableNames) {
+ override fun onInvalidated(tables: MutableSet<String>) {
+ observerChannel.offer(Unit)
}
- } finally {
- db.invalidationTracker.removeObserver(observer)
}
+ observerChannel.offer(Unit) // Initial signal to perform first query.
+ val queryContext = coroutineContext[TransactionElement]?.transactionDispatcher
+ ?: if (inTransaction) db.transactionDispatcher else db.queryDispatcher
+ val resultChannel = Channel<R>()
+ launch(queryContext) {
+ db.invalidationTracker.addObserver(observer)
+ try {
+ // Iterate until cancelled, transforming observer signals to query results
+ // to be emitted to the flow.
+ for (signal in observerChannel) {
+ val result = callable.call()
+ resultChannel.send(result)
+ }
+ } finally {
+ db.invalidationTracker.removeObserver(observer)
+ }
+ }
+
+ emitAll(resultChannel)
}
}
}
diff --git a/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
index 086b217..28c8f23 100644
--- a/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
+++ b/room/ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
@@ -20,8 +20,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.flow.single
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.Test
@@ -39,25 +38,29 @@
@Test
fun testCreateFlow() = testRun {
var callableExecuted = false
+ val expectedResult = Any()
val flow = CoroutinesRoom.createFlow(
db = database,
inTransaction = false,
tableNames = arrayOf("Pet"),
- callable = Callable { callableExecuted = true }
+ callable = Callable {
+ callableExecuted = true
+ expectedResult
+ }
)
assertThat(invalidationTracker.observers.isEmpty()).isTrue()
assertThat(callableExecuted).isFalse()
val job = async {
- flow.single()
+ flow.first()
}
yield(); yield() // yield for async and flow
assertThat(invalidationTracker.observers.size).isEqualTo(1)
assertThat(callableExecuted).isTrue()
- job.cancelAndJoin()
+ assertThat(job.await()).isEqualTo(expectedResult)
assertThat(invalidationTracker.observers.isEmpty()).isTrue()
}
diff --git a/settings.gradle b/settings.gradle
index 3fe280c..9bc7f250 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -288,6 +288,7 @@
includeProject(":compose:runtime:runtime-rxjava3", "compose/runtime/runtime-rxjava3", [BuildType.COMPOSE])
includeProject(":compose:runtime:runtime-rxjava3:runtime-rxjava3-samples", "compose/runtime/runtime-rxjava3/samples", [BuildType.COMPOSE])
includeProject(":compose:runtime:runtime-saveable", "compose/runtime/runtime-saveable", [BuildType.COMPOSE])
+includeProject(":compose:runtime:runtime-saveable-lint", "compose/runtime/runtime-saveable-lint", [BuildType.COMPOSE])
includeProject(":compose:runtime:runtime-saveable:runtime-saveable-samples", "compose/runtime/runtime-saveable/samples", [BuildType.COMPOSE])
includeProject(":compose:runtime:runtime:benchmark", "compose/runtime/runtime/compose-runtime-benchmark", [BuildType.COMPOSE])
includeProject(":compose:runtime:runtime:integration-tests", "compose/runtime/runtime/integration-tests", [BuildType.COMPOSE])
diff --git a/startup/integration-tests/first-library/src/main/AndroidManifest.xml b/startup/integration-tests/first-library/src/main/AndroidManifest.xml
index dc4bfdc..b5f96d8 100644
--- a/startup/integration-tests/first-library/src/main/AndroidManifest.xml
+++ b/startup/integration-tests/first-library/src/main/AndroidManifest.xml
@@ -32,7 +32,7 @@
<!-- Remove initializer for WorkManager on-demand initialization -->
<meta-data
- android:name="androidx.work.impl.WorkManagerInitializer"
+ android:name="androidx.work.WorkManagerInitializer"
android:value="@string/androidx_startup"
tools:node="remove" />
</provider>
diff --git a/startup/startup-runtime/api/current.txt b/startup/startup-runtime/api/current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/current.txt
+++ b/startup/startup-runtime/api/current.txt
@@ -7,6 +7,16 @@
method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
}
+ public class InitializationProvider extends android.content.ContentProvider {
+ ctor public InitializationProvider();
+ method public final int delete(android.net.Uri, String?, String![]?);
+ method public final String? getType(android.net.Uri);
+ method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public final boolean onCreate();
+ method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public interface Initializer<T> {
method public T create(android.content.Context);
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/api/public_plus_experimental_current.txt b/startup/startup-runtime/api/public_plus_experimental_current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/public_plus_experimental_current.txt
+++ b/startup/startup-runtime/api/public_plus_experimental_current.txt
@@ -7,6 +7,16 @@
method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
}
+ public class InitializationProvider extends android.content.ContentProvider {
+ ctor public InitializationProvider();
+ method public final int delete(android.net.Uri, String?, String![]?);
+ method public final String? getType(android.net.Uri);
+ method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public final boolean onCreate();
+ method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public interface Initializer<T> {
method public T create(android.content.Context);
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/api/restricted_current.txt b/startup/startup-runtime/api/restricted_current.txt
index 847659c..434ba26 100644
--- a/startup/startup-runtime/api/restricted_current.txt
+++ b/startup/startup-runtime/api/restricted_current.txt
@@ -7,6 +7,16 @@
method public boolean isEagerlyInitialized(Class<? extends androidx.startup.Initializer<?>>);
}
+ public class InitializationProvider extends android.content.ContentProvider {
+ ctor public InitializationProvider();
+ method public final int delete(android.net.Uri, String?, String![]?);
+ method public final String? getType(android.net.Uri);
+ method public final android.net.Uri? insert(android.net.Uri, android.content.ContentValues?);
+ method public final boolean onCreate();
+ method public final android.database.Cursor? query(android.net.Uri, String![]?, String?, String![]?, String?);
+ method public final int update(android.net.Uri, android.content.ContentValues?, String?, String![]?);
+ }
+
public interface Initializer<T> {
method public T create(android.content.Context);
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
diff --git a/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java b/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
index 4d07b2a..a14ce54 100644
--- a/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
+++ b/startup/startup-runtime/src/main/java/androidx/startup/InitializationProvider.java
@@ -25,18 +25,14 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
/**
* The {@link ContentProvider} which discovers {@link Initializer}s in an application and
* initializes them before {@link Application#onCreate()}.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-public final class InitializationProvider extends ContentProvider {
+public class InitializationProvider extends ContentProvider {
@Override
- public boolean onCreate() {
+ public final boolean onCreate() {
Context context = getContext();
if (context != null) {
AppInitializer.getInstance(context).discoverAndInitialize();
@@ -48,7 +44,7 @@
@Nullable
@Override
- public Cursor query(
+ public final Cursor query(
@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@@ -59,18 +55,18 @@
@Nullable
@Override
- public String getType(@NonNull Uri uri) {
+ public final String getType(@NonNull Uri uri) {
throw new IllegalStateException("Not allowed.");
}
@Nullable
@Override
- public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ public final Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
throw new IllegalStateException("Not allowed.");
}
@Override
- public int delete(
+ public final int delete(
@NonNull Uri uri,
@Nullable String selection,
@Nullable String[] selectionArgs) {
@@ -78,7 +74,7 @@
}
@Override
- public int update(
+ public final int update(
@NonNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
diff --git a/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt
new file mode 100644
index 0000000..3d26c96
--- /dev/null
+++ b/text/text/src/androidTest/java/androidx/compose/ui/text/android/TextLayoutIntrinsicWidthTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.android
+
+import android.text.Spannable
+import android.text.SpannableString
+import android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE
+import android.text.TextPaint
+import androidx.compose.ui.text.android.style.LetterSpacingSpanEm
+import androidx.compose.ui.text.android.style.LetterSpacingSpanPx
+import androidx.compose.ui.text.android.style.LineHeightSpan
+import androidx.compose.ui.text.font.test.R
+import androidx.core.content.res.ResourcesCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(InternalPlatformTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class TextLayoutIntrinsicWidthTest {
+ private val defaultText = SpannableString("This is a callout message")
+
+ // values are exact values for the repro case (on Pixel4, Android 11)
+ private val fontScale = 1.15f
+ private val density = 3.051f
+ private val letterSpacingEm = 0.4f / 12f
+ private val fontSize = 12f.spToPx()
+ private val letterSpacingPx = 0.4f.spToPx()
+ private val lineHeight = 16f.spToPx().toInt()
+ private lateinit var defaultPaint: TextPaint
+
+ @Before
+ fun setup() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ defaultPaint = TextPaint().apply {
+ typeface = ResourcesCompat.getFont(instrumentation.context, R.font.sample_font)!!
+ textSize = fontSize
+ }
+ }
+
+ @Test
+ fun intrinsicWidth_with_letterSpacing_and_lineHeight_createsOneLine() {
+ val text = defaultText.apply {
+ setSpan(LineHeightSpan(lineHeight))
+ setSpan(LetterSpacingSpanPx(letterSpacingPx))
+ }
+
+ assertLineCount(text)
+ }
+
+ @Test
+ fun intrinsicWidth_with_letterSpacing_and_lineHeight_createsOneLine_multipleSpans() {
+ val text = defaultText.apply {
+ for (i in 0..8) {
+ setSpan(LineHeightSpan(lineHeight), i, i + 1)
+ setSpan(LetterSpacingSpanPx(letterSpacingPx), i, i + 1)
+ }
+ }
+
+ assertLineCount(text)
+ }
+
+ @Test
+ fun intrinsicWidth_with_letterSpacingEm_and_lineHeight_createsOneLine() {
+ val text = defaultText.apply {
+ setSpan(LineHeightSpan(lineHeight))
+ setSpan(LetterSpacingSpanEm(letterSpacingEm))
+ }
+
+ assertLineCount(text)
+ }
+
+ @Test
+ fun intrinsicWidth_with_paintLetterSpacing_and_lineHeight_createsOneLine() {
+ val text = defaultText.apply {
+ setSpan(LineHeightSpan(lineHeight))
+ }
+
+ val paint = defaultPaint.apply {
+ letterSpacing = letterSpacingEm
+ }
+
+ assertLineCount(text, paint)
+ }
+
+ @Test
+ fun intrinsicWidth_with_letterSpacing_and_noLineHeight_createsOneLine() {
+ val text = defaultText.apply {
+ setSpan(LetterSpacingSpanPx(letterSpacingPx))
+ }
+
+ assertLineCount(text)
+ }
+
+ @Test
+ fun intrinsicWidth_with_noLetterSpacing_and_withLineHeight_createsOneLine() {
+ val text = defaultText.apply {
+ setSpan(LineHeightSpan(lineHeight))
+ }
+
+ assertLineCount(text)
+ }
+
+ @Test
+ fun intrinsicWidth_with_noLetterSpacing_and_noLineHeight_createsOneLine() {
+ assertLineCount(defaultText)
+ }
+
+ private fun assertLineCount(text: CharSequence, paint: TextPaint = defaultPaint) {
+ val intrinsics = LayoutIntrinsics(text, paint, LayoutCompat.TEXT_DIRECTION_LTR)
+ assertThat(
+ TextLayout(
+ charSequence = text,
+ width = intrinsics.maxIntrinsicWidth,
+ textPaint = paint
+ ).lineCount
+ ).isEqualTo(1)
+ }
+
+ fun Spannable.setSpan(span: Any, start: Int = 0, end: Int = length) {
+ this.setSpan(span, start, end, SPAN_INCLUSIVE_INCLUSIVE)
+ }
+
+ fun Float.spToPx(): Float = this * fontScale * density
+}
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt b/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
index b1fc875..942c5fd 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/CharSequenceCharacterIterator.kt
@@ -55,7 +55,7 @@
*
* @return the last character in the text, or [java.text.CharacterIterator.DONE] if the
* text is empty
- * @see .getEndIndex
+ * @see CharSequenceCharacterIterator.getEndIndex
*/
override fun last(): Char {
return if (start == end) {
@@ -73,7 +73,7 @@
* @return the character at the current position or [java.text.CharacterIterator.DONE]
* if the current
* position is off the end of the text.
- * @see .getIndex
+ * @see CharSequenceCharacterIterator.getIndex
*/
override fun current(): Char {
return if (index == end) CharacterIterator.DONE else charSequence[index]
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
index 0480714..65bb944 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutIntrinsics.kt
@@ -18,7 +18,11 @@
import android.text.BoringLayout
import android.text.Layout
+import android.text.Spanned
import android.text.TextPaint
+import androidx.compose.ui.text.android.style.LetterSpacingSpanEm
+import androidx.compose.ui.text.android.style.LetterSpacingSpanPx
+import androidx.compose.ui.text.android.style.LineHeightSpan
import java.text.BreakIterator
import java.util.PriorityQueue
@@ -37,7 +41,7 @@
* Compute Android platform BoringLayout metrics. A null value means the provided CharSequence
* cannot be laid out using a BoringLayout.
*/
- val boringMetrics: BoringLayout.Metrics? by lazy {
+ val boringMetrics: BoringLayout.Metrics? by lazy(LazyThreadSafetyMode.NONE) {
val frameworkTextDir = getTextDirectionHeuristic(textDirectionHeuristic)
BoringLayoutFactory.measure(charSequence, textPaint, frameworkTextDir)
}
@@ -47,7 +51,7 @@
*
* @see androidx.compose.ui.text.android.minIntrinsicWidth
*/
- val minIntrinsicWidth: Float by lazy {
+ val minIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
minIntrinsicWidth(charSequence, textPaint)
}
@@ -55,9 +59,15 @@
* Calculate maximum intrinsic width for the CharSequence. Maximum intrinsic width is the width
* of text where no soft line breaks are applied.
*/
- val maxIntrinsicWidth: Float by lazy {
- boringMetrics?.width?.toFloat()
+ val maxIntrinsicWidth: Float by lazy(LazyThreadSafetyMode.NONE) {
+ var desiredWidth: Float = boringMetrics?.width?.toFloat()
?: Layout.getDesiredWidth(charSequence, 0, charSequence.length, textPaint)
+ if (shouldIncreaseMaxIntrinsic(desiredWidth, charSequence, textPaint)) {
+ // b/173574230, increase maxIntrinsicWidth, so that StaticLayout won't form 2
+ // lines for the given maxIntrinsicWidth
+ desiredWidth += 0.5f
+ }
+ desiredWidth
}
}
@@ -107,3 +117,28 @@
return minWidth
}
+
+/**
+ * b/173574230
+ * on Android 11 and above, creating a StaticLayout when
+ * - desiredWidth is an Integer,
+ * - letterSpacing is set
+ * - lineHeight is set
+ * StaticLayout forms 2 lines for the given desiredWidth.
+ *
+ * This function checks if those conditions are met.
+ */
+@OptIn(InternalPlatformTextApi::class)
+private fun shouldIncreaseMaxIntrinsic(
+ desiredWidth: Float,
+ charSequence: CharSequence,
+ textPaint: TextPaint
+): Boolean {
+ return desiredWidth != 0f &&
+ charSequence is Spanned && (
+ textPaint.letterSpacing != 0f ||
+ charSequence.hasSpan(LetterSpacingSpanPx::class.java) ||
+ charSequence.hasSpan(LetterSpacingSpanEm::class.java)
+ ) &&
+ charSequence.hasSpan(LineHeightSpan::class.java)
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl b/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
similarity index 74%
copy from car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
copy to text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
index e022dc3..21b4eb4 100644
--- a/car/app/app/src/main/aidl/androidx/car/app/model/signin/IOnInputCompletedListener.aidl
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/SpannedExtensions.kt
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package androidx.car.app.model.signin;
+package androidx.compose.ui.text.android
-import androidx.car.app.IOnDoneCallback;
+import android.text.Spanned
-/** @hide */
-oneway interface IOnInputCompletedListener {
- void onInputCompleted(String value, IOnDoneCallback callback) = 1;
-}
+internal fun Spanned.hasSpan(clazz: Class<*>): Boolean {
+ return nextSpanTransition(-1 /* start */, length /* limit */, clazz) != length
+}
\ No newline at end of file
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 77edef6..1103629 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -160,7 +160,7 @@
start = 0,
end = charSequence.length,
paint = textPaint,
- width = ceil(width).toInt(),
+ width = widthInt,
textDir = frameworkTextDir,
alignment = frameworkAlignment,
maxLines = maxLines,
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
index d2ab8e8..7589408 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/style/LineHeightSpan.kt
@@ -17,6 +17,7 @@
import android.graphics.Paint.FontMetricsInt
import androidx.compose.ui.text.android.InternalPlatformTextApi
+import kotlin.math.ceil
/**
* The span which modifies the height of the covered paragraphs. A paragraph is defined as a
@@ -44,8 +45,7 @@
return
}
val ratio = this.lineHeight * 1.0f / currentHeight
- fontMetricsInt.descent =
- Math.ceil(fontMetricsInt.descent * ratio.toDouble()).toInt()
+ fontMetricsInt.descent = ceil(fontMetricsInt.descent * ratio.toDouble()).toInt()
fontMetricsInt.ascent = fontMetricsInt.descent - this.lineHeight
}
}
\ No newline at end of file
diff --git a/tv-provider/tv-provider/api/current.txt b/tv-provider/tv-provider/api/current.txt
index 885ce64..a48d742 100644
--- a/tv-provider/tv-provider/api/current.txt
+++ b/tv-provider/tv-provider/api/current.txt
@@ -75,7 +75,7 @@
@WorkerThread public class ChannelLogoUtils {
ctor @Deprecated public ChannelLogoUtils();
- method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+ method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
}
diff --git a/tv-provider/tv-provider/api/public_plus_experimental_current.txt b/tv-provider/tv-provider/api/public_plus_experimental_current.txt
index 885ce64..a48d742 100644
--- a/tv-provider/tv-provider/api/public_plus_experimental_current.txt
+++ b/tv-provider/tv-provider/api/public_plus_experimental_current.txt
@@ -75,7 +75,7 @@
@WorkerThread public class ChannelLogoUtils {
ctor @Deprecated public ChannelLogoUtils();
- method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+ method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
}
diff --git a/tv-provider/tv-provider/api/restricted_current.txt b/tv-provider/tv-provider/api/restricted_current.txt
index 8b4fe6af..089a93e 100644
--- a/tv-provider/tv-provider/api/restricted_current.txt
+++ b/tv-provider/tv-provider/api/restricted_current.txt
@@ -81,7 +81,7 @@
@WorkerThread public class ChannelLogoUtils {
ctor @Deprecated public ChannelLogoUtils();
- method @WorkerThread public static android.graphics.Bitmap! loadChannelLogo(android.content.Context, long);
+ method @WorkerThread public static android.graphics.Bitmap? loadChannelLogo(android.content.Context, long);
method public static boolean storeChannelLogo(android.content.Context, long, android.net.Uri);
method @WorkerThread public static boolean storeChannelLogo(android.content.Context, long, android.graphics.Bitmap);
}
diff --git a/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java b/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
index 8c24f11..83702d2 100644
--- a/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
+++ b/tv-provider/tv-provider/src/androidTest/java/androidx/tvprovider/media/tv/ChannelLogoUtilsTest.java
@@ -32,9 +32,9 @@
import android.net.Uri;
import android.os.SystemClock;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
-import androidx.test.filters.Suppress;
import androidx.tvprovider.test.R;
import org.junit.After;
@@ -42,7 +42,6 @@
import org.junit.Test;
import org.junit.runner.RunWith;
-@Suppress // Test is failing b/70905391
@MediumTest
@RunWith(AndroidJUnit4.class)
public class ChannelLogoUtilsTest {
@@ -54,6 +53,9 @@
@Before
public void setUp() throws Exception {
+ if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+ return;
+ }
mContentResolver = getApplicationContext().getContentResolver();
ContentValues contentValues = new Channel.Builder()
.setInputId(FAKE_INPUT_ID)
@@ -64,11 +66,17 @@
@After
public void tearDown() throws Exception {
+ if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+ return;
+ }
mContentResolver.delete(mChannelUri, null, null);
}
@Test
public void testStoreChannelLogo_fromBitmap() {
+ if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+ return;
+ }
assertNull(ChannelLogoUtils.loadChannelLogo(getApplicationContext(), mChannelId));
Bitmap logo = BitmapFactory.decodeResource(getApplicationContext().getResources(),
R.drawable.test_icon);
@@ -82,6 +90,9 @@
@Test
public void testStoreChannelLogo_fromResUri() {
+ if (!Utils.hasTvInputFramework(ApplicationProvider.getApplicationContext())) {
+ return;
+ }
assertNull(ChannelLogoUtils.loadChannelLogo(getApplicationContext(), mChannelId));
int resId = R.drawable.test_icon;
Resources res = getApplicationContext().getResources();
diff --git a/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java b/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
index df3753c..158e499 100644
--- a/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
+++ b/tv-provider/tv-provider/src/main/java/androidx/tvprovider/media/tv/ChannelLogoUtils.java
@@ -27,6 +27,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import java.io.FileNotFoundException;
@@ -150,16 +151,17 @@
* @see #storeChannelLogo(Context, long, Uri)
* @see #storeChannelLogo(Context, long, Bitmap)
*/
+ @Nullable
@WorkerThread
@SuppressLint("WrongThread") // TODO https://issuetracker.google.com/issues/116776070
public static Bitmap loadChannelLogo(@NonNull Context context, long channelId) {
Bitmap channelLogo = null;
- try {
- channelLogo = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(
- TvContract.buildChannelLogoUri(channelId)));
- } catch (FileNotFoundException e) {
+ Uri logoUri = TvContract.buildChannelLogoUri(channelId);
+ try (InputStream is = context.getContentResolver().openInputStream(logoUri)) {
+ channelLogo = BitmapFactory.decodeStream(is);
+ } catch (IOException e) {
// Channel logo is not found in the content provider.
- Log.i(TAG, "Channel logo for channel (ID:" + channelId + ") not found.", e);
+ Log.i(TAG, "Could not load channel logo for channel (ID:" + channelId + ").", e);
}
return channelLogo;
}
diff --git a/wear/tiles/tiles-renderer/api/current.txt b/wear/tiles/tiles-renderer/api/current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/current.txt
+++ b/wear/tiles/tiles-renderer/api/current.txt
@@ -11,43 +11,14 @@
package androidx.wear.tiles.renderer {
- public class ResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
- }
-
- public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
- }
-
- public static final class ResourceAccessors.Builder {
- method public androidx.wear.tiles.renderer.ResourceAccessors build();
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
- }
-
- public static interface ResourceAccessors.InlineImageResourceAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
- }
-
- public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
- ctor public ResourceAccessors.ResourceAccessException(String);
- ctor public ResourceAccessors.ResourceAccessException(String, Exception);
- }
-
- public class StandardResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
- }
-
public final class TileRenderer {
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
method public android.view.View? inflate(android.view.ViewGroup);
}
public static interface TileRenderer.LoadActionListener {
- method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+ method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
}
}
diff --git a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
+++ b/wear/tiles/tiles-renderer/api/public_plus_experimental_current.txt
@@ -11,43 +11,14 @@
package androidx.wear.tiles.renderer {
- public class ResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
- }
-
- public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
- }
-
- public static final class ResourceAccessors.Builder {
- method public androidx.wear.tiles.renderer.ResourceAccessors build();
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
- }
-
- public static interface ResourceAccessors.InlineImageResourceAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
- }
-
- public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
- ctor public ResourceAccessors.ResourceAccessException(String);
- ctor public ResourceAccessors.ResourceAccessException(String, Exception);
- }
-
- public class StandardResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
- }
-
public final class TileRenderer {
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
method public android.view.View? inflate(android.view.ViewGroup);
}
public static interface TileRenderer.LoadActionListener {
- method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+ method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
}
}
diff --git a/wear/tiles/tiles-renderer/api/restricted_current.txt b/wear/tiles/tiles-renderer/api/restricted_current.txt
index 1896e65..014a5a0 100644
--- a/wear/tiles/tiles-renderer/api/restricted_current.txt
+++ b/wear/tiles/tiles-renderer/api/restricted_current.txt
@@ -11,43 +11,14 @@
package androidx.wear.tiles.renderer {
- public class ResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder builder(androidx.wear.tiles.proto.ResourceProto.Resources);
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(String);
- }
-
- public static interface ResourceAccessors.AndroidImageResourceByResIdAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId);
- }
-
- public static final class ResourceAccessors.Builder {
- method public androidx.wear.tiles.renderer.ResourceAccessors build();
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setAndroidImageResourceByResIdAccessor(androidx.wear.tiles.renderer.ResourceAccessors.AndroidImageResourceByResIdAccessor);
- method public androidx.wear.tiles.renderer.ResourceAccessors.Builder setInlineImageResourceAccessor(androidx.wear.tiles.renderer.ResourceAccessors.InlineImageResourceAccessor);
- }
-
- public static interface ResourceAccessors.InlineImageResourceAccessor {
- method public com.google.common.util.concurrent.ListenableFuture<android.graphics.drawable.Drawable!> getDrawable(androidx.wear.tiles.proto.ResourceProto.InlineImageResource);
- }
-
- public static final class ResourceAccessors.ResourceAccessException extends java.lang.Exception {
- ctor public ResourceAccessors.ResourceAccessException(String);
- ctor public ResourceAccessors.ResourceAccessException(String, Exception);
- }
-
- public class StandardResourceAccessors {
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forLocalApp(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources);
- method public static androidx.wear.tiles.renderer.ResourceAccessors.Builder forRemoteService(android.content.Context, androidx.wear.tiles.builders.ResourceBuilders.Resources, android.content.res.Resources);
- }
-
public final class TileRenderer {
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
- ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.renderer.ResourceAccessors, @StyleRes int, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
+ ctor public TileRenderer(android.content.Context, androidx.wear.tiles.builders.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.builders.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
method public android.view.View? inflate(android.view.ViewGroup);
}
public static interface TileRenderer.LoadActionListener {
- method public void onClick(androidx.wear.tiles.proto.StateProto.State);
+ method public void onClick(androidx.wear.tiles.builders.StateBuilders.State);
}
}
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index ad0b0b7..5fb0cfb 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -40,7 +40,6 @@
import androidx.wear.tiles.proto.ResourceProto.InlineImageResource;
import androidx.wear.tiles.proto.ResourceProto.Resources;
import androidx.wear.tiles.protobuf.ByteString;
-import androidx.wear.tiles.renderer.StandardResourceAccessors;
import androidx.wear.tiles.renderer.TileRenderer;
import com.google.protobuf.TextFormat;
@@ -238,10 +237,7 @@
appContext,
LayoutElementBuilders.Layout.fromProto(
Layout.newBuilder().setRoot(rootElement).build()),
- StandardResourceAccessors.forLocalApp(
- appContext,
- ResourceBuilders.Resources.fromProto(generateResources()))
- .build(),
+ ResourceBuilders.Resources.fromProto(generateResources()),
ContextCompat.getMainExecutor(getApplicationContext()),
i -> {});
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
index 6bc0ffb..51095fd 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/manager/TileManager.kt
@@ -37,7 +37,6 @@
import androidx.wear.tiles.proto.DeviceParametersProto
import androidx.wear.tiles.proto.RequestProto
import androidx.wear.tiles.proto.StateProto
-import androidx.wear.tiles.renderer.StandardResourceAccessors
import androidx.wear.tiles.renderer.TileRenderer
import androidx.wear.tiles.timeline.TilesTimelineManager
import kotlinx.coroutines.CoroutineScope
@@ -184,9 +183,9 @@
val renderer = TileRenderer(
context,
layout,
- StandardResourceAccessors.forLocalApp(context, tileResources!!).build(),
+ tileResources!!,
ContextCompat.getMainExecutor(context),
- { state -> coroutineScope.launch { requestTile(state) } }
+ { state -> coroutineScope.launch { requestTile(state.toProto()) } }
)
renderer.inflate(parentView)?.apply {
(layoutParams as FrameLayout.LayoutParams).gravity = Gravity.CENTER
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 3736144..e77e338 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -16,100 +16,20 @@
package androidx.wear.tiles.renderer;
-import static java.lang.Math.max;
-import static java.lang.Math.round;
-
import android.content.Context;
-import android.content.Intent;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils.TruncateAt;
-import android.text.style.AbsoluteSizeSpan;
-import android.text.style.ClickableSpan;
-import android.text.style.ForegroundColorSpan;
-import android.text.style.ImageSpan;
-import android.text.style.MetricAffectingSpan;
-import android.text.style.StyleSpan;
-import android.text.style.UnderlineSpan;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.ContextThemeWrapper;
-import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewGroup.LayoutParams;
-import android.view.ViewOutlineProvider;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.ImageView.ScaleType;
-import android.widget.LinearLayout;
-import android.widget.Space;
-import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
-import androidx.core.content.ContextCompat;
-import androidx.wear.tiles.TileProviderService;
import androidx.wear.tiles.builders.LayoutElementBuilders;
-import androidx.wear.tiles.proto.ActionProto.Action;
-import androidx.wear.tiles.proto.ActionProto.AndroidActivity;
-import androidx.wear.tiles.proto.ActionProto.LaunchAction;
-import androidx.wear.tiles.proto.ActionProto.LoadAction;
-import androidx.wear.tiles.proto.DimensionProto.ContainerDimension;
-import androidx.wear.tiles.proto.DimensionProto.ContainerDimension.InnerCase;
-import androidx.wear.tiles.proto.DimensionProto.DpProp;
-import androidx.wear.tiles.proto.DimensionProto.ExpandedDimensionProp;
-import androidx.wear.tiles.proto.DimensionProto.ImageDimension;
-import androidx.wear.tiles.proto.DimensionProto.ProportionalDimensionProp;
-import androidx.wear.tiles.proto.DimensionProto.SpacerDimension;
-import androidx.wear.tiles.proto.DimensionProto.WrappedDimensionProp;
-import androidx.wear.tiles.proto.LayoutElementProto.Arc;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcAnchorTypeProp;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcLayoutElement;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcLine;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcSpacer;
-import androidx.wear.tiles.proto.LayoutElementProto.ArcText;
-import androidx.wear.tiles.proto.LayoutElementProto.Box;
-import androidx.wear.tiles.proto.LayoutElementProto.Column;
-import androidx.wear.tiles.proto.LayoutElementProto.ContentScaleMode;
-import androidx.wear.tiles.proto.LayoutElementProto.FontStyle;
-import androidx.wear.tiles.proto.LayoutElementProto.HorizontalAlignmentProp;
-import androidx.wear.tiles.proto.LayoutElementProto.Image;
-import androidx.wear.tiles.proto.LayoutElementProto.Layout;
-import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
-import androidx.wear.tiles.proto.LayoutElementProto.Row;
-import androidx.wear.tiles.proto.LayoutElementProto.Spacer;
-import androidx.wear.tiles.proto.LayoutElementProto.Span;
-import androidx.wear.tiles.proto.LayoutElementProto.SpanImage;
-import androidx.wear.tiles.proto.LayoutElementProto.SpanText;
-import androidx.wear.tiles.proto.LayoutElementProto.Spannable;
-import androidx.wear.tiles.proto.LayoutElementProto.Text;
-import androidx.wear.tiles.proto.LayoutElementProto.TextAlignmentProp;
-import androidx.wear.tiles.proto.LayoutElementProto.TextOverflowProp;
-import androidx.wear.tiles.proto.LayoutElementProto.VerticalAlignmentProp;
-import androidx.wear.tiles.proto.ModifiersProto.ArcModifiers;
-import androidx.wear.tiles.proto.ModifiersProto.Background;
-import androidx.wear.tiles.proto.ModifiersProto.Border;
-import androidx.wear.tiles.proto.ModifiersProto.Clickable;
-import androidx.wear.tiles.proto.ModifiersProto.Modifiers;
-import androidx.wear.tiles.proto.ModifiersProto.Padding;
-import androidx.wear.tiles.proto.ModifiersProto.SpanModifiers;
-import androidx.wear.tiles.proto.StateProto.State;
+import androidx.wear.tiles.builders.ResourceBuilders;
+import androidx.wear.tiles.builders.StateBuilders;
+import androidx.wear.tiles.renderer.internal.StandardResourceAccessors;
+import androidx.wear.tiles.renderer.internal.TileRendererInternal;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
-import java.util.concurrent.Future;
/**
* Renderer for Wear Tiles.
@@ -117,46 +37,6 @@
* <p>This variant uses Android views to represent the contents of the Wear Tile.
*/
public final class TileRenderer {
-
- private static final String TAG = "TileRenderer";
-
- private static final int HALIGN_DEFAULT_GRAVITY = Gravity.CENTER_HORIZONTAL;
- private static final int VALIGN_DEFAULT_GRAVITY = Gravity.CENTER_VERTICAL;
- private static final int TEXT_ALIGN_DEFAULT = Gravity.CENTER_HORIZONTAL;
- private static final ScaleType IMAGE_DEFAULT_SCALE_TYPE = ScaleType.FIT_CENTER;
-
- @WearArcLayout.LayoutParams.VerticalAlignment
- private static final int ARC_VALIGN_DEFAULT = WearArcLayout.LayoutParams.VALIGN_CENTER;
-
- // This is pretty badly named; TruncateAt specifies where to place the ellipsis (or whether to
- // marquee). Disabling truncation with null actually disables the _ellipsis_, but text will
- // still
- // be truncated.
- @Nullable private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
-
- private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
- private static final int TEXT_MAX_LINES_DEFAULT = 1;
- private static final int TEXT_MIN_LINES = 1;
-
- private static final ContainerDimension CONTAINER_DIMENSION_DEFAULT =
- ContainerDimension.newBuilder()
- .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
- .build();
-
- @WearArcLayout.AnchorType
- private static final int ARC_ANCHOR_DEFAULT = WearArcLayout.ANCHOR_CENTER;
-
- // White
- private static final int LINE_COLOR_DEFAULT = 0xFFFFFFFF;
-
- // Need to be package private so that TilesClickableSpan can see them.
- final Context mAppContext;
- final LoadActionListener mLoadActionListener;
- final Executor mLoadActionExecutor;
-
- private final Layout mLayout;
- private final ResourceAccessors mResourceAccessors;
-
/**
* Listener for clicks on Clickable objects that have an Action to (re)load the contents of a
* tile.
@@ -168,28 +48,30 @@
*
* @param nextState The state that the next tile should be in.
*/
- void onClick(@NonNull State nextState);
+ void onClick(@NonNull StateBuilders.State nextState);
}
+ private final TileRendererInternal mRenderer;
+
/**
* Default constructor.
*
* @param appContext The application context.
* @param layout The portion of the Tile to render.
- * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+ * @param resources The resources for the Tile.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
*/
public TileRenderer(
@NonNull Context appContext,
@NonNull LayoutElementBuilders.Layout layout,
- @NonNull ResourceAccessors resourceAccessors,
+ @NonNull ResourceBuilders.Resources resources,
@NonNull Executor loadActionExecutor,
@NonNull LoadActionListener loadActionListener) {
this(
appContext,
layout,
- resourceAccessors,
/* tilesTheme= */ 0,
+ resources,
loadActionExecutor,
loadActionListener);
}
@@ -199,1379 +81,25 @@
*
* @param appContext The application context.
* @param layout The portion of the Tile to render.
- * @param resourceAccessors Accessors for the resources used for rendering this Tile.
* @param tilesTheme The theme to use for this Tile instance. This can be used to customise
* things like the default font family. Pass 0 to use the default theme.
+ * @param resources The resources for the Tile.
* @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
*/
public TileRenderer(
@NonNull Context appContext,
@NonNull LayoutElementBuilders.Layout layout,
- @NonNull ResourceAccessors resourceAccessors,
@StyleRes int tilesTheme,
+ @NonNull ResourceBuilders.Resources resources,
@NonNull Executor loadActionExecutor,
@NonNull LoadActionListener loadActionListener) {
- if (tilesTheme == 0) {
- tilesTheme = R.style.TilesBaseTheme;
- }
-
- this.mAppContext = new ContextThemeWrapper(appContext, tilesTheme);
- this.mLayout = layout.toProto();
- this.mResourceAccessors = resourceAccessors;
- this.mLoadActionListener = loadActionListener;
- this.mLoadActionExecutor = loadActionExecutor;
- }
-
- private int safeDpToPx(DpProp dpProp) {
- return round(
- max(0, dpProp.getValue()) * mAppContext.getResources().getDisplayMetrics().density);
- }
-
- @Nullable
- private static Float safeAspectRatioOrNull(
- ProportionalDimensionProp proportionalDimensionProp) {
- final int dividend = proportionalDimensionProp.getAspectRatioWidth();
- final int divisor = proportionalDimensionProp.getAspectRatioHeight();
-
- if (dividend <= 0 || divisor <= 0) {
- return null;
- }
- return (float) dividend / divisor;
- }
-
- /**
- * Generates a generic LayoutParameters for use by all components. This just defaults to setting
- * the width/height to WRAP_CONTENT.
- *
- * @return The default layout parameters.
- */
- private static ViewGroup.LayoutParams generateDefaultLayoutParams() {
- return new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
- }
-
- private LayoutParams updateLayoutParamsInLinearLayout(
- LinearLayout parent,
- LayoutParams layoutParams,
- ContainerDimension width,
- ContainerDimension height) {
- // This is a little bit fun. Tiles' semantics is that dimension = expand should eat all
- // remaining space in that dimension, but not grow the parent. This is easy for standard
- // containers, but a little trickier in rows and columns on Android.
- //
- // A Row (LinearLayout) supports this with width=0 and weight>0. After doing a layout pass,
- // it
- // will assign all remaining space to elements with width=0 and weight>0, biased by the
- // weight.
- // This causes problems if there are two (or more) "expand" elements in a row, which is
- // itself
- // set to WRAP_CONTENTS, and one of those elements has a measured width (e.g. Text). In that
- // case, the LinearLayout will measure the text, then ensure that all elements with a weight
- // set
- // have their widths set according to the weight. For us, that means that _all_ elements
- // with
- // expand=true will size themselves to the same width as the Text, pushing out the bounds of
- // the
- // parent row. This happens on columns too, but of course regarding height.
- //
- // To get around this, if an element with expand=true is added to a row that is WRAP_CONTENT
- // (e.g. a row with no explicit width, that is not expanded), we ignore the expand=true, and
- // set the inner element's width to WRAP_CONTENT too.
-
- LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(layoutParams);
-
- // Handle the width
- if (parent.getOrientation() == LinearLayout.HORIZONTAL
- && width.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
- // If the parent container would not normally have "remaining space", ignore the
- // expand=true.
- if (parent.getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
- linearLayoutParams.width = LayoutParams.WRAP_CONTENT;
- } else {
- linearLayoutParams.width = 0;
- linearLayoutParams.weight = 1;
- }
- } else {
- linearLayoutParams.width = dimensionToPx(width);
- }
-
- // And the height
- if (parent.getOrientation() == LinearLayout.VERTICAL
- && height.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
- // If the parent container would not normally have "remaining space", ignore the
- // expand=true.
- if (parent.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
- linearLayoutParams.height = LayoutParams.WRAP_CONTENT;
- } else {
- linearLayoutParams.height = 0;
- linearLayoutParams.weight = 1;
- }
- } else {
- linearLayoutParams.height = dimensionToPx(height);
- }
-
- return linearLayoutParams;
- }
-
- private LayoutParams updateLayoutParams(
- ViewGroup parent,
- LayoutParams layoutParams,
- ContainerDimension width,
- ContainerDimension height) {
- if (parent instanceof LinearLayout) {
- // LinearLayouts have a bunch of messy caveats in Tile when their children can be
- // expanded;
- // factor that case out to keep this clean.
- return updateLayoutParamsInLinearLayout(
- (LinearLayout) parent, layoutParams, width, height);
- } else {
- layoutParams.width = dimensionToPx(width);
- layoutParams.height = dimensionToPx(height);
- }
-
- return layoutParams;
- }
-
- private static int horizontalAlignmentToGravity(HorizontalAlignmentProp alignment) {
- switch (alignment.getValue()) {
- case HALIGN_START:
- return Gravity.START;
- case HALIGN_CENTER:
- return Gravity.CENTER_HORIZONTAL;
- case HALIGN_END:
- return Gravity.END;
- case HALIGN_LEFT:
- return Gravity.LEFT;
- case HALIGN_RIGHT:
- return Gravity.RIGHT;
- case UNRECOGNIZED:
- case HALIGN_UNDEFINED:
- return HALIGN_DEFAULT_GRAVITY;
- }
-
- return HALIGN_DEFAULT_GRAVITY;
- }
-
- private static int verticalAlignmentToGravity(VerticalAlignmentProp alignment) {
- switch (alignment.getValue()) {
- case VALIGN_TOP:
- return Gravity.TOP;
- case VALIGN_CENTER:
- return Gravity.CENTER_VERTICAL;
- case VALIGN_BOTTOM:
- return Gravity.BOTTOM;
- case UNRECOGNIZED:
- case VALIGN_UNDEFINED:
- return VALIGN_DEFAULT_GRAVITY;
- }
-
- return VALIGN_DEFAULT_GRAVITY;
- }
-
- @WearArcLayout.LayoutParams.VerticalAlignment
- private static int verticalAlignmentToArcVAlign(VerticalAlignmentProp alignment) {
- switch (alignment.getValue()) {
- case VALIGN_TOP:
- return WearArcLayout.LayoutParams.VALIGN_OUTER;
- case VALIGN_CENTER:
- return WearArcLayout.LayoutParams.VALIGN_CENTER;
- case VALIGN_BOTTOM:
- return WearArcLayout.LayoutParams.VALIGN_INNER;
- case UNRECOGNIZED:
- case VALIGN_UNDEFINED:
- return ARC_VALIGN_DEFAULT;
- }
-
- return ARC_VALIGN_DEFAULT;
- }
-
- private static ScaleType contentScaleModeToScaleType(ContentScaleMode contentScaleMode) {
- switch (contentScaleMode) {
- case CONTENT_SCALE_MODE_FIT:
- return ScaleType.FIT_CENTER;
- case CONTENT_SCALE_MODE_CROP:
- return ScaleType.CENTER_CROP;
- case CONTENT_SCALE_MODE_FILL_BOUNDS:
- return ScaleType.FIT_XY;
- case CONTENT_SCALE_MODE_UNDEFINED:
- case UNRECOGNIZED:
- return IMAGE_DEFAULT_SCALE_TYPE;
- }
-
- return IMAGE_DEFAULT_SCALE_TYPE;
- }
-
- private static boolean isBold(FontStyle fontStyle) {
- // Although this method could be a simple equality check against FONT_WEIGHT_BOLD, we list
- // all
- // current cases here so that this will become a compile time error as soon as a new
- // FontWeight
- // value is added to the schema. If this fails to build, then this means that an int
- // typeface
- // style is no longer enough to represent all FontWeight values and a customizable,
- // per-weight
- // text style must be introduced to TileRenderer to handle this. See b/176980535
- switch (fontStyle.getWeight().getValue()) {
- case FONT_WEIGHT_BOLD:
- return true;
- case FONT_WEIGHT_NORMAL:
- case FONT_WEIGHT_UNDEFINED:
- case UNRECOGNIZED:
- return false;
- }
-
- return false;
- }
-
- private static int fontStyleToTypefaceStyle(FontStyle fontStyle) {
- final boolean isBold = isBold(fontStyle);
- final boolean isItalic = fontStyle.getItalic().getValue();
-
- if (isBold && isItalic) {
- return Typeface.BOLD_ITALIC;
- } else if (isBold) {
- return Typeface.BOLD;
- } else if (isItalic) {
- return Typeface.ITALIC;
- } else {
- return Typeface.NORMAL;
- }
- }
-
- @SuppressWarnings("nullness")
- private static Typeface createTypeface(
- FontStyle fontStyle, @Nullable Typeface currentTypeface) {
- return Typeface.create(currentTypeface, fontStyleToTypefaceStyle(fontStyle));
- }
-
- private static MetricAffectingSpan createTypefaceSpan(FontStyle fontStyle) {
- return new StyleSpan(fontStyleToTypefaceStyle(fontStyle));
- }
-
- private static boolean hasDefaultTypeface(FontStyle fontStyle) {
- return !fontStyle.getItalic().getValue() && !isBold(fontStyle);
- }
-
- private static void applyFontStyle(FontStyle style, TextView textView) {
- Typeface currentTypeface = textView.getTypeface();
-
- if (!hasDefaultTypeface(style)) {
- // Need to supply typefaceStyle when creating the typeface (will select specialist
- // bold/italic
- // typefaces), *and* when setting the typeface (will set synthetic bold/italic flags in
- // Paint
- // if they're not supported by the given typeface).
- textView.setTypeface(
- createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
- }
-
- int currentPaintFlags = textView.getPaintFlags();
-
- // Remove the bits we're setting
- currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
-
- if (style.hasUnderline() && style.getUnderline().getValue()) {
- currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
- }
-
- textView.setPaintFlags(currentPaintFlags);
-
- if (style.hasSize()) {
- textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.getSize().getValue());
- }
-
- if (style.hasLetterSpacing()) {
- textView.setLetterSpacing(style.getLetterSpacing().getValue());
- }
-
- textView.setTextColor(extractTextColorArgb(style));
- }
-
- private void applyClickable(View view, Clickable clickable) {
- view.setTag(clickable.getId());
-
- boolean hasAction = false;
- switch (clickable.getOnClick().getValueCase()) {
- case LAUNCH_ACTION:
- Intent i =
- buildLaunchActionIntent(
- clickable.getOnClick().getLaunchAction(), clickable.getId());
- if (i != null) {
- hasAction = true;
- view.setOnClickListener(
- v -> {
- if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
- mAppContext.startActivity(i);
- }
- });
- }
- break;
- case LOAD_ACTION:
- hasAction = true;
- view.setOnClickListener(
- v ->
- mLoadActionListener.onClick(
- buildState(
- clickable.getOnClick().getLoadAction(),
- clickable.getId())));
- break;
- case VALUE_NOT_SET:
- break;
- }
-
- if (hasAction) {
- // Apply ripple effect
- TypedValue outValue = new TypedValue();
- mAppContext
- .getTheme()
- .resolveAttribute(
- android.R.attr.selectableItemBackground,
- outValue,
- /* resolveRefs= */ true);
- view.setForeground(mAppContext.getDrawable(outValue.resourceId));
- }
- }
-
- private void applyPadding(View view, Padding padding) {
- if (padding.getRtlAware().getValue()) {
- view.setPaddingRelative(
- safeDpToPx(padding.getStart()),
- safeDpToPx(padding.getTop()),
- safeDpToPx(padding.getEnd()),
- safeDpToPx(padding.getBottom()));
- } else {
- view.setPadding(
- safeDpToPx(padding.getStart()),
- safeDpToPx(padding.getTop()),
- safeDpToPx(padding.getEnd()),
- safeDpToPx(padding.getBottom()));
- }
- }
-
- private GradientDrawable applyBackground(
- View view, Background background, @Nullable GradientDrawable drawable) {
- if (drawable == null) {
- drawable = new GradientDrawable();
- }
-
- if (background.hasColor()) {
- drawable.setColor(background.getColor().getArgb());
- }
-
- if (background.hasCorner()) {
- drawable.setCornerRadius(safeDpToPx(background.getCorner().getRadius()));
- view.setClipToOutline(true);
- view.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
- }
-
- return drawable;
- }
-
- private GradientDrawable applyBorder(Border border, @Nullable GradientDrawable drawable) {
- if (drawable == null) {
- drawable = new GradientDrawable();
- }
-
- drawable.setStroke(safeDpToPx(border.getWidth()), border.getColor().getArgb());
-
- return drawable;
- }
-
- private View applyModifiers(View view, Modifiers modifiers) {
- if (modifiers.hasClickable()) {
- applyClickable(view, modifiers.getClickable());
- }
-
- if (modifiers.hasSemantics()) {
- applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
- }
-
- if (modifiers.hasPadding()) {
- applyPadding(view, modifiers.getPadding());
- }
-
- GradientDrawable backgroundDrawable = null;
-
- if (modifiers.hasBackground()) {
- backgroundDrawable =
- applyBackground(view, modifiers.getBackground(), backgroundDrawable);
- }
-
- if (modifiers.hasBorder()) {
- backgroundDrawable = applyBorder(modifiers.getBorder(), backgroundDrawable);
- }
-
- if (backgroundDrawable != null) {
- view.setBackground(backgroundDrawable);
- }
-
- return view;
- }
-
- // This is a little nasty; ArcLayoutWidget is just an interface, so we have no guarantee that
- // the
- // instance also extends View (as it should). Instead, just take a View in and rename this, and
- // check that it's an ArcLayoutWidget internally.
- private View applyModifiersToArcLayoutView(View view, ArcModifiers modifiers) {
- if (!(view instanceof WearArcLayout.ArcLayoutWidget)) {
- Log.e(
- TAG,
- "applyModifiersToArcLayoutView should only be called with an ArcLayoutWidget");
- return view;
- }
-
- if (modifiers.hasClickable()) {
- applyClickable(view, modifiers.getClickable());
- }
-
- if (modifiers.hasSemantics()) {
- applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
- }
-
- return view;
- }
-
- private void applyFontStyle(FontStyle style, WearCurvedTextView textView) {
- Typeface currentTypeface = textView.getTypeface();
-
- if (!hasDefaultTypeface(style)) {
- // Need to supply typefaceStyle when creating the typeface (will select specialist
- // bold/italic
- // typefaces), *and* when setting the typeface (will set synthetic bold/italic flags in
- // Paint
- // if they're not supported by the given typeface).
- textView.setTypeface(
- createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
- }
-
- int currentPaintFlags = textView.getPaintFlags();
-
- // Remove the bits we're setting
- currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
-
- if (style.hasUnderline() && style.getUnderline().getValue()) {
- currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
- }
-
- textView.setPaintFlags(currentPaintFlags);
-
- if (style.hasSize()) {
- textView.setTextSize(
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
- style.getSize().getValue(),
- mAppContext.getResources().getDisplayMetrics()));
- }
- }
-
- private static int textAlignToAndroidGravity(TextAlignmentProp alignment) {
- switch (alignment.getValue()) {
- case TEXT_ALIGN_START:
- return Gravity.START;
- case TEXT_ALIGN_CENTER:
- return Gravity.CENTER_HORIZONTAL;
- case TEXT_ALIGN_END:
- return Gravity.END;
- case TEXT_ALIGN_UNDEFINED:
- case UNRECOGNIZED:
- return TEXT_ALIGN_DEFAULT;
- }
-
- return TEXT_ALIGN_DEFAULT;
- }
-
- @Nullable
- private static TruncateAt textTruncationToEllipsize(TextOverflowProp type) {
- switch (type.getValue()) {
- case TEXT_OVERFLOW_TRUNCATE:
- // A null TruncateAt disables adding an ellipsis.
- return null;
- case TEXT_OVERFLOW_ELLIPSIZE_END:
- return TruncateAt.END;
- case TEXT_OVERFLOW_UNDEFINED:
- case UNRECOGNIZED:
- return TEXT_OVERFLOW_DEFAULT;
- }
-
- return TEXT_OVERFLOW_DEFAULT;
- }
-
- @WearArcLayout.AnchorType
- private static int anchorTypeToAnchorPos(ArcAnchorTypeProp type) {
- switch (type.getValue()) {
- case ARC_ANCHOR_START:
- return WearArcLayout.ANCHOR_START;
- case ARC_ANCHOR_CENTER:
- return WearArcLayout.ANCHOR_CENTER;
- case ARC_ANCHOR_END:
- return WearArcLayout.ANCHOR_END;
- case ARC_ANCHOR_UNDEFINED:
- case UNRECOGNIZED:
- return ARC_ANCHOR_DEFAULT;
- }
-
- return ARC_ANCHOR_DEFAULT;
- }
-
- private int dimensionToPx(ContainerDimension containerDimension) {
- switch (containerDimension.getInnerCase()) {
- case LINEAR_DIMENSION:
- return safeDpToPx(containerDimension.getLinearDimension());
- case EXPANDED_DIMENSION:
- return LayoutParams.MATCH_PARENT;
- case WRAPPED_DIMENSION:
- return LayoutParams.WRAP_CONTENT;
- case INNER_NOT_SET:
- return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
- }
-
- return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
- }
-
- private static int extractTextColorArgb(FontStyle fontStyle) {
- if (fontStyle.hasColor()) {
- return fontStyle.getColor().getArgb();
- } else {
- return TEXT_COLOR_DEFAULT;
- }
- }
-
- /**
- * Returns an Android {@link Intent} that can perform the action defined in the given tile
- * {@link LaunchAction}.
- */
- @Nullable
- static Intent buildLaunchActionIntent(
- @NonNull LaunchAction launchAction, @NonNull String clickableId) {
- if (launchAction.hasAndroidActivity()) {
- AndroidActivity activity = launchAction.getAndroidActivity();
- Intent i =
- new Intent().setClassName(activity.getPackageName(), activity.getClassName());
- i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
- if (!clickableId.isEmpty()) {
- i.putExtra(TileProviderService.EXTRA_CLICKABLE_ID, clickableId);
- }
-
- return i;
- }
-
- return null;
- }
-
- static State buildState(LoadAction loadAction, String clickableId) {
- // Get the state specified by the provider and add the last clicked clickable's ID to it.
- return loadAction.getRequestState().toBuilder().setLastClickableId(clickableId).build();
- }
-
- @Nullable
- private View inflateColumn(ViewGroup parent, Column column) {
- ContainerDimension width =
- column.hasWidth() ? column.getWidth() : CONTAINER_DIMENSION_DEFAULT;
- ContainerDimension height =
- column.hasHeight() ? column.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
- if (!canMeasureContainer(width, height, column.getContentsList())) {
- Log.w(TAG, "Column set to wrap but contents are unmeasurable. Ignoring.");
- return null;
- }
-
- LinearLayout linearLayout = new LinearLayout(mAppContext);
- linearLayout.setOrientation(LinearLayout.VERTICAL);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- linearLayout.setGravity(horizontalAlignmentToGravity(column.getHorizontalAlignment()));
-
- layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
- View wrappedView = applyModifiers(linearLayout, column.getModifiers());
-
- parent.addView(wrappedView, layoutParams);
- inflateLayoutElements(linearLayout, column.getContentsList());
-
- return wrappedView;
- }
-
- @Nullable
- private View inflateRow(ViewGroup parent, Row row) {
- ContainerDimension width = row.hasWidth() ? row.getWidth() : CONTAINER_DIMENSION_DEFAULT;
- ContainerDimension height = row.hasHeight() ? row.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
- if (!canMeasureContainer(width, height, row.getContentsList())) {
- Log.w(TAG, "Row set to wrap but contents are unmeasurable. Ignoring.");
- return null;
- }
-
- LinearLayout linearLayout = new LinearLayout(mAppContext);
- linearLayout.setOrientation(LinearLayout.HORIZONTAL);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- linearLayout.setGravity(verticalAlignmentToGravity(row.getVerticalAlignment()));
-
- layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
- View wrappedView = applyModifiers(linearLayout, row.getModifiers());
-
- parent.addView(wrappedView, layoutParams);
- inflateLayoutElements(linearLayout, row.getContentsList());
-
- return wrappedView;
- }
-
- @Nullable
- private View inflateBox(ViewGroup parent, Box box) {
- ContainerDimension width = box.hasWidth() ? box.getWidth() : CONTAINER_DIMENSION_DEFAULT;
- ContainerDimension height = box.hasHeight() ? box.getHeight() : CONTAINER_DIMENSION_DEFAULT;
-
- if (!canMeasureContainer(width, height, box.getContentsList())) {
- Log.w(TAG, "Box set to wrap but contents are unmeasurable. Ignoring.");
- return null;
- }
-
- FrameLayout frame = new FrameLayout(mAppContext);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- layoutParams = updateLayoutParams(parent, layoutParams, width, height);
-
- int gravity =
- horizontalAlignmentToGravity(box.getHorizontalAlignment())
- | verticalAlignmentToGravity(box.getVerticalAlignment());
-
- View wrappedView = applyModifiers(frame, box.getModifiers());
-
- parent.addView(wrappedView, layoutParams);
- inflateLayoutElements(frame, box.getContentsList());
-
- // We can't set layout gravity to a FrameLayout ahead of time (and foregroundGravity only
- // sets
- // the gravity of the foreground Drawable). Go and apply gravity to the child.
- applyGravityToFrameLayoutChildren(frame, gravity);
-
- return wrappedView;
- }
-
- @Nullable
- private View inflateSpacer(ViewGroup parent, Spacer spacer) {
- int widthPx = safeDpToPx(spacer.getWidth().getLinearDimension());
- int heightPx = safeDpToPx(spacer.getHeight().getLinearDimension());
-
- if (widthPx == 0 && heightPx == 0) {
- return null;
- }
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- // Modifiers cannot be applied to android's Space, so use a plain View if this Spacer has
- // modifiers.
- View view;
- if (spacer.hasModifiers()) {
- view = applyModifiers(new View(mAppContext), spacer.getModifiers());
- layoutParams =
- updateLayoutParams(
- parent,
- layoutParams,
- spacerDimensionToContainerDimension(spacer.getWidth()),
- spacerDimensionToContainerDimension(spacer.getHeight()));
- } else {
- view = new Space(mAppContext);
- view.setMinimumWidth(widthPx);
- view.setMinimumHeight(heightPx);
- }
-
- parent.addView(view, layoutParams);
-
- return view;
- }
-
- @Nullable
- private View inflateArcSpacer(ViewGroup parent, ArcSpacer spacer) {
- float lengthDegrees = max(0, spacer.getLength().getValue());
- int thicknessPx = safeDpToPx(spacer.getThickness());
-
- if (lengthDegrees == 0 && thicknessPx == 0) {
- return null;
- }
-
- WearCurvedSpacer space = new WearCurvedSpacer(mAppContext);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- space.setSweepAngleDegrees(lengthDegrees);
- space.setThicknessPx(thicknessPx);
-
- View wrappedView = applyModifiersToArcLayoutView(space, spacer.getModifiers());
- parent.addView(wrappedView, layoutParams);
-
- return wrappedView;
- }
-
- private View inflateText(ViewGroup parent, Text text) {
- TextView textView =
- new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- textView.setText(text.getText().getValue());
-
- textView.setEllipsize(textTruncationToEllipsize(text.getOverflow()));
- textView.setGravity(textAlignToAndroidGravity(text.getMultilineAlignment()));
-
- if (text.hasMaxLines()) {
- textView.setMaxLines(max(TEXT_MIN_LINES, text.getMaxLines().getValue()));
- } else {
- textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
- }
-
- // Setting colours **must** go after setting the Text Appearance, otherwise it will get
- // immediately overridden.
- if (text.hasFontStyle()) {
- applyFontStyle(text.getFontStyle(), textView);
- } else {
- applyFontStyle(FontStyle.getDefaultInstance(), textView);
- }
-
- if (text.hasLineHeight()) {
- float lineHeight =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
- text.getLineHeight().getValue(),
- mAppContext.getResources().getDisplayMetrics());
- final float fontHeight = textView.getPaint().getFontSpacing();
- if (lineHeight != fontHeight) {
- textView.setLineSpacing(lineHeight - fontHeight, 1f);
- }
- }
-
- View wrappedView = applyModifiers(textView, text.getModifiers());
- parent.addView(wrappedView, layoutParams);
-
- // We don't want the text to be screen-reader focusable, unless wrapped in a Audible. This
- // prevents automatically reading out partial text (e.g. text in a row) etc.
- textView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
-
- return wrappedView;
- }
-
- private View inflateArcText(ViewGroup parent, ArcText text) {
- WearCurvedTextView textView =
- new WearCurvedTextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
- layoutParams.width = LayoutParams.MATCH_PARENT;
- layoutParams.height = LayoutParams.MATCH_PARENT;
-
- textView.setText(text.getText().getValue());
-
- if (text.hasFontStyle()) {
- applyFontStyle(text.getFontStyle(), textView);
- }
-
- textView.setTextColor(extractTextColorArgb(text.getFontStyle()));
-
- View wrappedView = applyModifiersToArcLayoutView(textView, text.getModifiers());
- parent.addView(wrappedView, layoutParams);
-
- return wrappedView;
- }
-
- private static boolean isZeroLengthImageDimension(ImageDimension dimension) {
- return dimension.getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION
- && dimension.getLinearDimension().getValue() == 0;
- }
-
- private static ContainerDimension imageDimensionToContainerDimension(ImageDimension dimension) {
- switch (dimension.getInnerCase()) {
- case LINEAR_DIMENSION:
- return ContainerDimension.newBuilder()
- .setLinearDimension(dimension.getLinearDimension())
- .build();
- case EXPANDED_DIMENSION:
- return ContainerDimension.newBuilder()
- .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance())
- .build();
- case PROPORTIONAL_DIMENSION:
- // A ratio size should be translated to a WRAP_CONTENT; the RatioViewWrapper will
- // deal with
- // the sizing of that.
- return ContainerDimension.newBuilder()
- .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
- .build();
- case INNER_NOT_SET:
- break;
- }
- // Caller should have already checked for this.
- throw new IllegalArgumentException(
- "ImageDimension has an unknown dimension type: " + dimension.getInnerCase().name());
- }
-
- private static ContainerDimension spacerDimensionToContainerDimension(
- SpacerDimension dimension) {
- switch (dimension.getInnerCase()) {
- case LINEAR_DIMENSION:
- return ContainerDimension.newBuilder()
- .setLinearDimension(dimension.getLinearDimension())
- .build();
- case INNER_NOT_SET:
- // A spacer is allowed to have missing dimension and this should be considered as
- // 0dp.
- return ContainerDimension.newBuilder()
- .setLinearDimension(DpProp.getDefaultInstance())
- .build();
- }
- // Caller should have already checked for this.
- throw new IllegalArgumentException(
- "SpacerDimension has an unknown dimension type: "
- + dimension.getInnerCase().name());
- }
-
- @SuppressWarnings("ExecutorTaskName")
- @Nullable
- private View inflateImage(ViewGroup parent, Image image) {
- String protoResId = image.getResourceId().getValue();
-
- // If either width or height isn't set, abort.
- if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET
- || image.getHeight().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET) {
- Log.w(TAG, "One of width and height not set on image " + protoResId);
- return null;
- }
-
- // The image must occupy _some_ space.
- if (isZeroLengthImageDimension(image.getWidth())
- || isZeroLengthImageDimension(image.getHeight())) {
- Log.w(TAG, "One of width and height was zero on image " + protoResId);
- return null;
- }
-
- // Both dimensions can't be ratios.
- if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION
- && image.getHeight().getInnerCase()
- == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
- Log.w(TAG, "Both width and height were proportional for image " + protoResId);
- return null;
- }
-
- // Pull the ratio for the RatioViewWrapper. Was either argument a proportional dimension?
- @Nullable Float ratio = RatioViewWrapper.UNDEFINED_ASPECT_RATIO;
-
- if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
- ratio = safeAspectRatioOrNull(image.getWidth().getProportionalDimension());
- }
-
- if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
- ratio = safeAspectRatioOrNull(image.getHeight().getProportionalDimension());
- }
-
- if (ratio == null) {
- Log.w(TAG, "Invalid aspect ratio for image " + protoResId);
- return null;
- }
-
- ImageView imageView = new ImageView(mAppContext);
-
- if (image.hasContentScaleMode()) {
- imageView.setScaleType(
- contentScaleModeToScaleType(image.getContentScaleMode().getValue()));
- }
-
- if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
- imageView.setMinimumWidth(safeDpToPx(image.getWidth().getLinearDimension()));
- }
-
- if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
- imageView.setMinimumHeight(safeDpToPx(image.getHeight().getLinearDimension()));
- }
-
- // We need to sort out the sizing of the widget now, so we can pass the correct params to
- // RatioViewWrapper. First, translate the ImageSize to a ContainerSize. A ratio size should
- // be translated to a WRAP_CONTENT; the RatioViewWrapper will deal with the sizing of that.
- LayoutParams ratioWrapperLayoutParams = generateDefaultLayoutParams();
- ratioWrapperLayoutParams =
- updateLayoutParams(
- parent,
- ratioWrapperLayoutParams,
- imageDimensionToContainerDimension(image.getWidth()),
- imageDimensionToContainerDimension(image.getHeight()));
-
- RatioViewWrapper ratioViewWrapper = new RatioViewWrapper(mAppContext);
- ratioViewWrapper.setAspectRatio(ratio);
- ratioViewWrapper.addView(imageView);
-
- // Finally, wrap the image in any modifiers...
- View wrappedView = applyModifiers(ratioViewWrapper, image.getModifiers());
-
- parent.addView(wrappedView, ratioWrapperLayoutParams);
-
- ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
- if (drawableFuture.isDone()) {
- // If the future is done, immediately draw.
- setImageDrawable(imageView, drawableFuture, protoResId);
- } else {
- // Otherwise, handle the result on the UI thread.
- drawableFuture.addListener(
- () -> setImageDrawable(imageView, drawableFuture, protoResId),
- ContextCompat.getMainExecutor(mAppContext));
- }
-
- return wrappedView;
- }
-
- private static void setImageDrawable(
- ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
- try {
- imageView.setImageDrawable(drawableFuture.get());
- } catch (ExecutionException | InterruptedException e) {
- Log.w(TAG, "Could not get drawable for image " + protoResId);
- }
- }
-
- @Nullable
- private View inflateArcLine(ViewGroup parent, ArcLine line) {
- float lengthDegrees = max(0, line.getLength().getValue());
- int thicknessPx = safeDpToPx(line.getThickness());
-
- if (lengthDegrees == 0 && thicknessPx == 0) {
- return null;
- }
-
- WearCurvedLineView lineView = new WearCurvedLineView(mAppContext);
-
- // A ArcLineView must always be the same width/height as its parent, so it can draw the line
- // properly inside of those bounds.
- LayoutParams layoutParams = generateDefaultLayoutParams();
- layoutParams.width = LayoutParams.MATCH_PARENT;
- layoutParams.height = LayoutParams.MATCH_PARENT;
-
- int lineColor = LINE_COLOR_DEFAULT;
- if (line.hasColor()) {
- lineColor = line.getColor().getArgb();
- }
-
- lineView.setThicknessPx(thicknessPx);
- lineView.setSweepAngleDegrees(lengthDegrees);
- lineView.setColor(lineColor);
-
- View wrappedView = applyModifiersToArcLayoutView(lineView, line.getModifiers());
- parent.addView(wrappedView, layoutParams);
-
- return wrappedView;
- }
-
- @Nullable
- private View inflateArc(ViewGroup parent, Arc arc) {
- WearArcLayout arcLayout = new WearArcLayout(mAppContext);
-
- LayoutParams layoutParams = generateDefaultLayoutParams();
- layoutParams.width = LayoutParams.MATCH_PARENT;
- layoutParams.height = LayoutParams.MATCH_PARENT;
-
- arcLayout.setAnchorAngleDegrees(arc.getAnchorAngle().getValue());
- arcLayout.setAnchorType(anchorTypeToAnchorPos(arc.getAnchorType()));
-
- // Add all children.
- for (ArcLayoutElement child : arc.getContentsList()) {
- @Nullable View childView = inflateArcLayoutElement(arcLayout, child);
- if (childView != null) {
- WearArcLayout.LayoutParams childLayoutParams =
- (WearArcLayout.LayoutParams) childView.getLayoutParams();
- boolean rotate = false;
- if (child.hasAdapter()) {
- rotate = child.getAdapter().getRotateContents().getValue();
- }
-
- // Apply rotation and gravity.
- childLayoutParams.setRotate(rotate);
- childLayoutParams.setVerticalAlignment(
- verticalAlignmentToArcVAlign(arc.getVerticalAlign()));
- }
- }
-
- View wrappedView = applyModifiers(arcLayout, arc.getModifiers());
- parent.addView(wrappedView, layoutParams);
-
- return wrappedView;
- }
-
- private void applyStylesToSpan(
- SpannableStringBuilder builder, int start, int end, FontStyle fontStyle) {
- if (fontStyle.hasSize()) {
- float fontSize =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
- fontStyle.getSize().getValue(),
- mAppContext.getResources().getDisplayMetrics());
-
- AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(fontSize));
- builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
- }
-
- if (!hasDefaultTypeface(fontStyle)) {
- MetricAffectingSpan span = createTypefaceSpan(fontStyle);
- builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
- }
-
- if (fontStyle.getUnderline().getValue()) {
- UnderlineSpan span = new UnderlineSpan();
- builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
- }
-
- if (fontStyle.hasLetterSpacing()) {
- LetterSpacingSpan span = new LetterSpacingSpan(fontStyle.getLetterSpacing().getValue());
- builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
- }
-
- ForegroundColorSpan colorSpan;
-
- colorSpan = new ForegroundColorSpan(extractTextColorArgb(fontStyle));
-
- builder.setSpan(colorSpan, start, end, Spanned.SPAN_MARK_MARK);
- }
-
- private void applyModifiersToSpan(
- SpannableStringBuilder builder, int start, int end, SpanModifiers modifiers) {
- if (modifiers.hasClickable()) {
- ClickableSpan clickableSpan = new TilesClickableSpan(modifiers.getClickable());
-
- builder.setSpan(clickableSpan, start, end, Spanned.SPAN_MARK_MARK);
- }
- }
-
- private SpannableStringBuilder inflateTextInSpannable(
- SpannableStringBuilder builder, SpanText text) {
- int currentPos = builder.length();
- int lastPos = currentPos + text.getText().getValue().length();
-
- builder.append(text.getText().getValue());
-
- applyStylesToSpan(builder, currentPos, lastPos, text.getFontStyle());
- applyModifiersToSpan(builder, currentPos, lastPos, text.getModifiers());
-
- return builder;
- }
-
- @SuppressWarnings("ExecutorTaskName")
- private SpannableStringBuilder inflateImageInSpannable(
- SpannableStringBuilder builder, SpanImage protoImage, TextView textView) {
- String protoResId = protoImage.getResourceId().getValue();
-
- if (protoImage.getWidth().getValue() == 0 || protoImage.getHeight().getValue() == 0) {
- Log.w(TAG, "One of width and height was zero on image " + protoResId);
- return builder;
- }
-
- ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
- if (drawableFuture.isDone()) {
- // If the future is done, immediately add drawable to builder.
- try {
- Drawable drawable = drawableFuture.get();
- appendSpanDrawable(builder, drawable, protoImage);
- } catch (ExecutionException | InterruptedException e) {
- Log.w(
- TAG,
- "Could not get drawable for image "
- + protoImage.getResourceId().getValue());
- }
- } else {
- // If the future is not done, add an empty drawable to builder as a placeholder.
- Drawable emptyDrawable = new ColorDrawable(Color.TRANSPARENT);
- int startInclusive = builder.length();
- ImageSpan emptyDrawableSpan = appendSpanDrawable(builder, emptyDrawable, protoImage);
- int endExclusive = builder.length();
-
- // When the future is done, replace the empty drawable with the received one.
- drawableFuture.addListener(
- () -> {
- // Remove the placeholder. This should be safe, even with other modifiers
- // applied. This
- // just removes the single drawable span, and should leave other spans in
- // place.
- builder.removeSpan(emptyDrawableSpan);
- // Add the new drawable to the same range.
- setSpanDrawable(
- builder, drawableFuture, startInclusive, endExclusive, protoImage);
- // Update the TextView.
- textView.setText(builder);
- },
- ContextCompat.getMainExecutor(mAppContext));
- }
-
- return builder;
- }
-
- private ImageSpan appendSpanDrawable(
- SpannableStringBuilder builder, Drawable drawable, SpanImage protoImage) {
- drawable.setBounds(
- 0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
- ImageSpan imgSpan = new ImageSpan(drawable);
-
- int startPos = builder.length();
- builder.append(" ", imgSpan, Spanned.SPAN_MARK_MARK);
- int endPos = builder.length();
-
- applyModifiersToSpan(builder, startPos, endPos, protoImage.getModifiers());
-
- return imgSpan;
- }
-
- private void setSpanDrawable(
- SpannableStringBuilder builder,
- ListenableFuture<Drawable> drawableFuture,
- int startInclusive,
- int endExclusive,
- SpanImage protoImage) {
- final String protoResourceId = protoImage.getResourceId().getValue();
-
- try {
- // Add the image span to the same range occupied by the placeholder.
- Drawable drawable = drawableFuture.get();
- drawable.setBounds(
- 0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
- ImageSpan imgSpan = new ImageSpan(drawable);
- builder.setSpan(
- imgSpan,
- startInclusive,
- endExclusive,
- android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
- } catch (ExecutionException | InterruptedException e) {
- Log.w(TAG, "Could not get drawable for image " + protoResourceId);
- }
- }
-
- private View inflateSpannable(ViewGroup parent, Spannable spannable) {
- TextView tv = new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
- LayoutParams layoutParams = generateDefaultLayoutParams();
-
- SpannableStringBuilder builder = new SpannableStringBuilder();
-
- for (Span element : spannable.getSpansList()) {
- switch (element.getInnerCase()) {
- case IMAGE:
- SpanImage protoImage = element.getImage();
- builder = inflateImageInSpannable(builder, protoImage, tv);
- break;
- case TEXT:
- SpanText protoText = element.getText();
- builder = inflateTextInSpannable(builder, protoText);
- break;
- default:
- Log.w(TAG, "Unknown Span child type.");
- break;
- }
- }
-
- tv.setEllipsize(textTruncationToEllipsize(spannable.getOverflow()));
- tv.setGravity(horizontalAlignmentToGravity(spannable.getMultilineAlignment()));
-
- if (spannable.hasMaxLines()) {
- tv.setMaxLines(max(TEXT_MIN_LINES, spannable.getMaxLines().getValue()));
- } else {
- tv.setMaxLines(TEXT_MAX_LINES_DEFAULT);
- }
-
- if (spannable.hasLineSpacing()) {
- float lineSpacing =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,
- spannable.getLineSpacing().getValue(),
- mAppContext.getResources().getDisplayMetrics());
- tv.setLineSpacing(lineSpacing, 1f);
- }
-
- tv.setText(builder);
-
- View wrappedView = applyModifiers(tv, spannable.getModifiers());
- parent.addView(applyModifiers(tv, spannable.getModifiers()), layoutParams);
-
- return wrappedView;
- }
-
- @Nullable
- private View inflateArcLayoutElement(ViewGroup parent, ArcLayoutElement element) {
- View inflatedView = null;
-
- switch (element.getInnerCase()) {
- case ADAPTER:
- // Fall back to the normal inflater.
- inflatedView = inflateLayoutElement(parent, element.getAdapter().getContent());
- break;
-
- case SPACER:
- inflatedView = inflateArcSpacer(parent, element.getSpacer());
- break;
-
- case LINE:
- inflatedView = inflateArcLine(parent, element.getLine());
- break;
-
- case TEXT:
- inflatedView = inflateArcText(parent, element.getText());
- break;
-
- case INNER_NOT_SET:
- break;
- }
-
- if (inflatedView == null) {
- // Covers null (returned when the childCase in the proto isn't known). Sadly, ProtoLite
- // doesn't give us a way to access childCase's underlying tag, so we can't give any
- // smarter
- // error message here.
- Log.w(TAG, "Unknown child type");
- }
-
- return inflatedView;
- }
-
- @Nullable
- private View inflateLayoutElement(ViewGroup parent, LayoutElement element) {
- // What is it?
- View inflatedView = null;
- switch (element.getInnerCase()) {
- case COLUMN:
- inflatedView = inflateColumn(parent, element.getColumn());
- break;
- case ROW:
- inflatedView = inflateRow(parent, element.getRow());
- break;
- case BOX:
- inflatedView = inflateBox(parent, element.getBox());
- break;
- case SPACER:
- inflatedView = inflateSpacer(parent, element.getSpacer());
- break;
- case TEXT:
- inflatedView = inflateText(parent, element.getText());
- break;
- case IMAGE:
- inflatedView = inflateImage(parent, element.getImage());
- break;
- case ARC:
- inflatedView = inflateArc(parent, element.getArc());
- break;
- case SPANNABLE:
- inflatedView = inflateSpannable(parent, element.getSpannable());
- break;
- case INNER_NOT_SET:
- default: // TODO(b/178359365): Remove default case
- Log.w(TAG, "Unknown child type: " + element.getInnerCase().name());
- break;
- }
-
- return inflatedView;
- }
-
- private boolean canMeasureContainer(
- ContainerDimension containerWidth,
- ContainerDimension containerHeight,
- List<LayoutElement> elements) {
- // We can't measure a container if it's set to wrap-contents but all of its contents are set
- // to
- // expand-to-parent. Such containers must not be displayed.
- if (containerWidth.hasWrappedDimension() && !containsMeasurableWidth(elements)) {
- return false;
- }
- if (containerHeight.hasWrappedDimension() && !containsMeasurableHeight(elements)) {
- return false;
- }
- return true;
- }
-
- private boolean containsMeasurableWidth(List<LayoutElement> elements) {
- for (LayoutElement element : elements) {
- if (isWidthMeasurable(element)) {
- // Enough to find a single element that is measurable.
- return true;
- }
- }
- return false;
- }
-
- private boolean containsMeasurableHeight(List<LayoutElement> elements) {
- for (LayoutElement element : elements) {
- if (isHeightMeasurable(element)) {
- // Enough to find a single element that is measurable.
- return true;
- }
- }
- return false;
- }
-
- private boolean isWidthMeasurable(LayoutElement element) {
- switch (element.getInnerCase()) {
- case COLUMN:
- return isMeasurable(element.getColumn().getWidth());
- case ROW:
- return isMeasurable(element.getRow().getWidth());
- case BOX:
- return isMeasurable(element.getBox().getWidth());
- case SPACER:
- return isMeasurable(element.getSpacer().getWidth());
- case IMAGE:
- return isMeasurable(element.getImage().getWidth());
- case ARC:
- case TEXT:
- case SPANNABLE:
- return true;
- case INNER_NOT_SET:
- default: // TODO(b/178359365): Remove default case
- return false;
- }
- }
-
- private boolean isHeightMeasurable(LayoutElement element) {
- switch (element.getInnerCase()) {
- case COLUMN:
- return isMeasurable(element.getColumn().getHeight());
- case ROW:
- return isMeasurable(element.getRow().getHeight());
- case BOX:
- return isMeasurable(element.getBox().getHeight());
- case SPACER:
- return isMeasurable(element.getSpacer().getHeight());
- case IMAGE:
- return isMeasurable(element.getImage().getHeight());
- case ARC:
- case TEXT:
- case SPANNABLE:
- return true;
- case INNER_NOT_SET:
- default: // TODO(b/178359365): Remove default case
- return false;
- }
- }
-
- private boolean isMeasurable(ContainerDimension dimension) {
- return dimensionToPx(dimension) != LayoutParams.MATCH_PARENT;
- }
-
- private static boolean isMeasurable(ImageDimension dimension) {
- switch (dimension.getInnerCase()) {
- case LINEAR_DIMENSION:
- case PROPORTIONAL_DIMENSION:
- return true;
- case EXPANDED_DIMENSION:
- case INNER_NOT_SET:
- return false;
- }
- return false;
- }
-
- private static boolean isMeasurable(SpacerDimension dimension) {
- switch (dimension.getInnerCase()) {
- case LINEAR_DIMENSION:
- return true;
- case INNER_NOT_SET:
- return false;
- }
- return false;
- }
-
- private void inflateLayoutElements(ViewGroup parent, List<LayoutElement> elements) {
- for (LayoutElement element : elements) {
- inflateLayoutElement(parent, element);
- }
+ this.mRenderer = new TileRendererInternal(
+ appContext,
+ layout.toProto(),
+ StandardResourceAccessors.forLocalApp(appContext, resources).build(),
+ tilesTheme,
+ loadActionExecutor,
+ (s) -> loadActionListener.onClick(StateBuilders.State.fromProto(s)));
}
/**
@@ -1584,70 +112,6 @@
*/
@Nullable
public View inflate(@NonNull ViewGroup parent) {
- // Go!
- return inflateLayoutElement(parent, mLayout.getRoot());
- }
-
- private static void applyGravityToFrameLayoutChildren(FrameLayout parent, int gravity) {
- for (int i = 0; i < parent.getChildCount(); i++) {
- View child = parent.getChildAt(i);
-
- // All children should have a LayoutParams already set...
- if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) {
- // This...shouldn't happen.
- throw new IllegalStateException(
- "Layout params of child is not a descendant of FrameLayout.LayoutParams.");
- }
-
- // Children should grow out from the middle of the layout.
- ((FrameLayout.LayoutParams) child.getLayoutParams()).gravity = gravity;
- }
- }
-
- private static void applyAudibleParams(View view, String accessibilityLabel) {
- view.setContentDescription(accessibilityLabel);
- view.setFocusable(true);
- view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
-
- /** Implementation of ClickableSpan for Tiles' Clickables. */
- private class TilesClickableSpan extends ClickableSpan {
- private final Clickable mClickable;
-
- TilesClickableSpan(Clickable clickable) {
- this.mClickable = clickable;
- }
-
- @Override
- public void onClick(@NonNull View widget) {
- Action action = mClickable.getOnClick();
-
- switch (action.getValueCase()) {
- case LAUNCH_ACTION:
- Intent i =
- buildLaunchActionIntent(action.getLaunchAction(), mClickable.getId());
- if (i != null) {
- if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
- mAppContext.startActivity(i);
- }
- }
- break;
- case LOAD_ACTION:
- mLoadActionExecutor.execute(
- () ->
- mLoadActionListener.onClick(
- buildState(
- action.getLoadAction(), mClickable.getId())));
-
- break;
- case VALUE_NOT_SET:
- break;
- }
- }
-
- @Override
- public void updateDrawState(@NonNull TextPaint ds) {
- // Don't change the underlying text appearance.
- }
+ return mRenderer.inflate(parent);
}
}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
similarity index 94%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
index 8b02a0b..2f46f99 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/AndroidResourceAccessor.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/AndroidResourceAccessor.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.annotation.SuppressLint;
import android.content.res.Resources;
@@ -22,7 +22,6 @@
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.wear.tiles.proto.ResourceProto.AndroidImageResourceByResId;
@@ -30,10 +29,7 @@
/**
* Resource accessor for Android resources.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class AndroidResourceAccessor
implements ResourceAccessors.AndroidImageResourceByResIdAccessor {
private final Resources mAndroidResources;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
similarity index 96%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
index fc8f2d5..6a6964c 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/InlineResourceAccessor.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/InlineResourceAccessor.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.annotation.SuppressLint;
import android.content.Context;
@@ -25,7 +25,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.concurrent.futures.ResolvableFuture;
import androidx.wear.tiles.proto.ResourceProto.ImageFormat;
import androidx.wear.tiles.proto.ResourceProto.InlineImageResource;
@@ -36,10 +35,7 @@
/**
* Resource accessor for inline resources.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class InlineResourceAccessor implements ResourceAccessors.InlineImageResourceAccessor {
private static final int RGB565_BYTES_PER_PX = 2;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
similarity index 91%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
index 951f2c65..642be88 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/LetterSpacingSpan.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/LetterSpacingSpan.java
@@ -14,20 +14,16 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
/**
* LetterSpacingSpan class used to apply custom spacing between letters.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class LetterSpacingSpan extends MetricAffectingSpan {
private final float mLetterSpacingEm;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
index 25e642e..2623851 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/RatioViewWrapper.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/RatioViewWrapper.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.content.Context;
import android.util.AttributeSet;
@@ -24,7 +24,6 @@
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
import androidx.annotation.StyleRes;
/**
@@ -43,10 +42,7 @@
*
* <p>Note that if both axes are exact, this container does nothing; it will simply size the child
* and itself according to the exact MeasureSpecs.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class RatioViewWrapper extends ViewGroup {
/**
* An undefined aspect ratio. If {@link #setAspectRatio} is called with this value, or never
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
index 2f8ee24..b420042 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/ResourceAccessors.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/ResourceAccessors.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.annotation.SuppressLint;
import android.graphics.drawable.Drawable;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
similarity index 98%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
index 9e0b4a2..dad88b8 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/StandardResourceAccessors.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/StandardResourceAccessors.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.content.Context;
import android.content.res.Resources;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
new file mode 100644
index 0000000..1637bca
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/TileRendererInternal.java
@@ -0,0 +1,1633 @@
+/*
+ * 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.tiles.renderer.internal;
+
+import static java.lang.Math.max;
+import static java.lang.Math.round;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Typeface;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils.TruncateAt;
+import android.text.style.AbsoluteSizeSpan;
+import android.text.style.ClickableSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.ImageSpan;
+import android.text.style.MetricAffectingSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.Space;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.core.content.ContextCompat;
+import androidx.wear.tiles.TileProviderService;
+import androidx.wear.tiles.proto.ActionProto.Action;
+import androidx.wear.tiles.proto.ActionProto.AndroidActivity;
+import androidx.wear.tiles.proto.ActionProto.LaunchAction;
+import androidx.wear.tiles.proto.ActionProto.LoadAction;
+import androidx.wear.tiles.proto.DimensionProto.ContainerDimension;
+import androidx.wear.tiles.proto.DimensionProto.ContainerDimension.InnerCase;
+import androidx.wear.tiles.proto.DimensionProto.DpProp;
+import androidx.wear.tiles.proto.DimensionProto.ExpandedDimensionProp;
+import androidx.wear.tiles.proto.DimensionProto.ImageDimension;
+import androidx.wear.tiles.proto.DimensionProto.ProportionalDimensionProp;
+import androidx.wear.tiles.proto.DimensionProto.SpacerDimension;
+import androidx.wear.tiles.proto.DimensionProto.WrappedDimensionProp;
+import androidx.wear.tiles.proto.LayoutElementProto.Arc;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcAnchorTypeProp;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcLayoutElement;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcLine;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcSpacer;
+import androidx.wear.tiles.proto.LayoutElementProto.ArcText;
+import androidx.wear.tiles.proto.LayoutElementProto.Box;
+import androidx.wear.tiles.proto.LayoutElementProto.Column;
+import androidx.wear.tiles.proto.LayoutElementProto.ContentScaleMode;
+import androidx.wear.tiles.proto.LayoutElementProto.FontStyle;
+import androidx.wear.tiles.proto.LayoutElementProto.HorizontalAlignmentProp;
+import androidx.wear.tiles.proto.LayoutElementProto.Image;
+import androidx.wear.tiles.proto.LayoutElementProto.Layout;
+import androidx.wear.tiles.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.tiles.proto.LayoutElementProto.Row;
+import androidx.wear.tiles.proto.LayoutElementProto.Spacer;
+import androidx.wear.tiles.proto.LayoutElementProto.Span;
+import androidx.wear.tiles.proto.LayoutElementProto.SpanImage;
+import androidx.wear.tiles.proto.LayoutElementProto.SpanText;
+import androidx.wear.tiles.proto.LayoutElementProto.Spannable;
+import androidx.wear.tiles.proto.LayoutElementProto.Text;
+import androidx.wear.tiles.proto.LayoutElementProto.TextAlignmentProp;
+import androidx.wear.tiles.proto.LayoutElementProto.TextOverflowProp;
+import androidx.wear.tiles.proto.LayoutElementProto.VerticalAlignmentProp;
+import androidx.wear.tiles.proto.ModifiersProto.ArcModifiers;
+import androidx.wear.tiles.proto.ModifiersProto.Background;
+import androidx.wear.tiles.proto.ModifiersProto.Border;
+import androidx.wear.tiles.proto.ModifiersProto.Clickable;
+import androidx.wear.tiles.proto.ModifiersProto.Modifiers;
+import androidx.wear.tiles.proto.ModifiersProto.Padding;
+import androidx.wear.tiles.proto.ModifiersProto.SpanModifiers;
+import androidx.wear.tiles.proto.StateProto.State;
+import androidx.wear.tiles.renderer.R;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+
+/**
+ * Renderer for Wear Tiles.
+ *
+ * <p>This variant uses Android views to represent the contents of the Wear Tile.
+ */
+public final class TileRendererInternal {
+
+ private static final String TAG = "TileRenderer";
+
+ private static final int HALIGN_DEFAULT_GRAVITY = Gravity.CENTER_HORIZONTAL;
+ private static final int VALIGN_DEFAULT_GRAVITY = Gravity.CENTER_VERTICAL;
+ private static final int TEXT_ALIGN_DEFAULT = Gravity.CENTER_HORIZONTAL;
+ private static final ScaleType IMAGE_DEFAULT_SCALE_TYPE = ScaleType.FIT_CENTER;
+
+ @WearArcLayout.LayoutParams.VerticalAlignment
+ private static final int ARC_VALIGN_DEFAULT = WearArcLayout.LayoutParams.VALIGN_CENTER;
+
+ // This is pretty badly named; TruncateAt specifies where to place the ellipsis (or whether to
+ // marquee). Disabling truncation with null actually disables the _ellipsis_, but text will
+ // still be truncated.
+ @Nullable private static final TruncateAt TEXT_OVERFLOW_DEFAULT = null;
+
+ private static final int TEXT_COLOR_DEFAULT = 0xFFFFFFFF;
+ private static final int TEXT_MAX_LINES_DEFAULT = 1;
+ private static final int TEXT_MIN_LINES = 1;
+
+ private static final ContainerDimension CONTAINER_DIMENSION_DEFAULT =
+ ContainerDimension.newBuilder()
+ .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
+ .build();
+
+ @WearArcLayout.AnchorType
+ private static final int ARC_ANCHOR_DEFAULT = WearArcLayout.ANCHOR_CENTER;
+
+ // White
+ private static final int LINE_COLOR_DEFAULT = 0xFFFFFFFF;
+
+ // Need to be package private so that TilesClickableSpan can see them.
+ final Context mAppContext;
+ final LoadActionListener mLoadActionListener;
+ final Executor mLoadActionExecutor;
+
+ private final Layout mLayout;
+ private final ResourceAccessors mResourceAccessors;
+
+ /**
+ * Listener for clicks on Clickable objects that have an Action to (re)load the contents of a
+ * tile.
+ */
+ public interface LoadActionListener {
+
+ /**
+ * Called when a Clickable that has a LoadAction is clicked.
+ *
+ * @param nextState The state that the next tile should be in.
+ */
+ void onClick(@NonNull State nextState);
+ }
+
+ /**
+ * Default constructor.
+ *
+ * @param appContext The application context.
+ * @param layout The portion of the Tile to render.
+ * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+ * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
+ */
+ public TileRendererInternal(
+ @NonNull Context appContext,
+ @NonNull Layout layout,
+ @NonNull ResourceAccessors resourceAccessors,
+ @NonNull Executor loadActionExecutor,
+ @NonNull LoadActionListener loadActionListener) {
+ this(
+ appContext,
+ layout,
+ resourceAccessors,
+ /* tilesTheme= */ 0,
+ loadActionExecutor,
+ loadActionListener);
+ }
+
+ /**
+ * Default constructor.
+ *
+ * @param appContext The application context.
+ * @param layout The portion of the Tile to render.
+ * @param resourceAccessors Accessors for the resources used for rendering this Tile.
+ * @param tilesTheme The theme to use for this Tile instance. This can be used to customise
+ * things like the default font family. Pass 0 to use the default theme.
+ * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
+ */
+ public TileRendererInternal(
+ @NonNull Context appContext,
+ @NonNull Layout layout,
+ @NonNull ResourceAccessors resourceAccessors,
+ @StyleRes int tilesTheme,
+ @NonNull Executor loadActionExecutor,
+ @NonNull LoadActionListener loadActionListener) {
+ if (tilesTheme == 0) {
+ tilesTheme = R.style.TilesBaseTheme;
+ }
+
+ this.mAppContext = new ContextThemeWrapper(appContext, tilesTheme);
+ this.mLayout = layout;
+ this.mResourceAccessors = resourceAccessors;
+ this.mLoadActionListener = loadActionListener;
+ this.mLoadActionExecutor = loadActionExecutor;
+ }
+
+ private int safeDpToPx(DpProp dpProp) {
+ return round(
+ max(0, dpProp.getValue()) * mAppContext.getResources().getDisplayMetrics().density);
+ }
+
+ @Nullable
+ private static Float safeAspectRatioOrNull(
+ ProportionalDimensionProp proportionalDimensionProp) {
+ final int dividend = proportionalDimensionProp.getAspectRatioWidth();
+ final int divisor = proportionalDimensionProp.getAspectRatioHeight();
+
+ if (dividend <= 0 || divisor <= 0) {
+ return null;
+ }
+ return (float) dividend / divisor;
+ }
+
+ /**
+ * Generates a generic LayoutParameters for use by all components. This just defaults to setting
+ * the width/height to WRAP_CONTENT.
+ *
+ * @return The default layout parameters.
+ */
+ private static ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ private LayoutParams updateLayoutParamsInLinearLayout(
+ LinearLayout parent,
+ LayoutParams layoutParams,
+ ContainerDimension width,
+ ContainerDimension height) {
+ // This is a little bit fun. Tiles' semantics is that dimension = expand should eat all
+ // remaining space in that dimension, but not grow the parent. This is easy for standard
+ // containers, but a little trickier in rows and columns on Android.
+ //
+ // A Row (LinearLayout) supports this with width=0 and weight>0. After doing a layout pass,
+ // it will assign all remaining space to elements with width=0 and weight>0, biased by
+ // the weight. This causes problems if there are two (or more) "expand" elements in a
+ // row, which is itself set to WRAP_CONTENTS, and one of those elements has a measured
+ // width (e.g. Text). In that case, the LinearLayout will measure the text, then ensure
+ // that all elements with a weight set have their widths set according to the weight. For
+ // us, that means that _all_ elements with expand=true will size themselves to the same
+ // width as the Text, pushing out the bounds of the parent row. This happens on columns
+ // too, but of course regarding height.
+ //
+ // To get around this, if an element with expand=true is added to a row that is WRAP_CONTENT
+ // (e.g. a row with no explicit width, that is not expanded), we ignore the expand=true, and
+ // set the inner element's width to WRAP_CONTENT too.
+
+ LinearLayout.LayoutParams linearLayoutParams = new LinearLayout.LayoutParams(layoutParams);
+
+ // Handle the width
+ if (parent.getOrientation() == LinearLayout.HORIZONTAL
+ && width.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
+ // If the parent container would not normally have "remaining space", ignore the
+ // expand=true.
+ if (parent.getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
+ linearLayoutParams.width = LayoutParams.WRAP_CONTENT;
+ } else {
+ linearLayoutParams.width = 0;
+ linearLayoutParams.weight = 1;
+ }
+ } else {
+ linearLayoutParams.width = dimensionToPx(width);
+ }
+
+ // And the height
+ if (parent.getOrientation() == LinearLayout.VERTICAL
+ && height.getInnerCase() == InnerCase.EXPANDED_DIMENSION) {
+ // If the parent container would not normally have "remaining space", ignore the
+ // expand=true.
+ if (parent.getLayoutParams().height == LayoutParams.WRAP_CONTENT) {
+ linearLayoutParams.height = LayoutParams.WRAP_CONTENT;
+ } else {
+ linearLayoutParams.height = 0;
+ linearLayoutParams.weight = 1;
+ }
+ } else {
+ linearLayoutParams.height = dimensionToPx(height);
+ }
+
+ return linearLayoutParams;
+ }
+
+ private LayoutParams updateLayoutParams(
+ ViewGroup parent,
+ LayoutParams layoutParams,
+ ContainerDimension width,
+ ContainerDimension height) {
+ if (parent instanceof LinearLayout) {
+ // LinearLayouts have a bunch of messy caveats in Tile when their children can be
+ // expanded; factor that case out to keep this clean.
+ return updateLayoutParamsInLinearLayout(
+ (LinearLayout) parent, layoutParams, width, height);
+ } else {
+ layoutParams.width = dimensionToPx(width);
+ layoutParams.height = dimensionToPx(height);
+ }
+
+ return layoutParams;
+ }
+
+ private static int horizontalAlignmentToGravity(HorizontalAlignmentProp alignment) {
+ switch (alignment.getValue()) {
+ case HALIGN_START:
+ return Gravity.START;
+ case HALIGN_CENTER:
+ return Gravity.CENTER_HORIZONTAL;
+ case HALIGN_END:
+ return Gravity.END;
+ case HALIGN_LEFT:
+ return Gravity.LEFT;
+ case HALIGN_RIGHT:
+ return Gravity.RIGHT;
+ case UNRECOGNIZED:
+ case HALIGN_UNDEFINED:
+ return HALIGN_DEFAULT_GRAVITY;
+ }
+
+ return HALIGN_DEFAULT_GRAVITY;
+ }
+
+ private static int verticalAlignmentToGravity(VerticalAlignmentProp alignment) {
+ switch (alignment.getValue()) {
+ case VALIGN_TOP:
+ return Gravity.TOP;
+ case VALIGN_CENTER:
+ return Gravity.CENTER_VERTICAL;
+ case VALIGN_BOTTOM:
+ return Gravity.BOTTOM;
+ case UNRECOGNIZED:
+ case VALIGN_UNDEFINED:
+ return VALIGN_DEFAULT_GRAVITY;
+ }
+
+ return VALIGN_DEFAULT_GRAVITY;
+ }
+
+ @WearArcLayout.LayoutParams.VerticalAlignment
+ private static int verticalAlignmentToArcVAlign(VerticalAlignmentProp alignment) {
+ switch (alignment.getValue()) {
+ case VALIGN_TOP:
+ return WearArcLayout.LayoutParams.VALIGN_OUTER;
+ case VALIGN_CENTER:
+ return WearArcLayout.LayoutParams.VALIGN_CENTER;
+ case VALIGN_BOTTOM:
+ return WearArcLayout.LayoutParams.VALIGN_INNER;
+ case UNRECOGNIZED:
+ case VALIGN_UNDEFINED:
+ return ARC_VALIGN_DEFAULT;
+ }
+
+ return ARC_VALIGN_DEFAULT;
+ }
+
+ private static ScaleType contentScaleModeToScaleType(ContentScaleMode contentScaleMode) {
+ switch (contentScaleMode) {
+ case CONTENT_SCALE_MODE_FIT:
+ return ScaleType.FIT_CENTER;
+ case CONTENT_SCALE_MODE_CROP:
+ return ScaleType.CENTER_CROP;
+ case CONTENT_SCALE_MODE_FILL_BOUNDS:
+ return ScaleType.FIT_XY;
+ case CONTENT_SCALE_MODE_UNDEFINED:
+ case UNRECOGNIZED:
+ return IMAGE_DEFAULT_SCALE_TYPE;
+ }
+
+ return IMAGE_DEFAULT_SCALE_TYPE;
+ }
+
+ private static boolean isBold(FontStyle fontStyle) {
+ // Although this method could be a simple equality check against FONT_WEIGHT_BOLD, we list
+ // all current cases here so that this will become a compile time error as soon as a new
+ // FontWeight value is added to the schema. If this fails to build, then this means that
+ // an int typeface style is no longer enough to represent all FontWeight values and a
+ // customizable, per-weight text style must be introduced to TileRenderer to handle this.
+ // See b/176980535
+ switch (fontStyle.getWeight().getValue()) {
+ case FONT_WEIGHT_BOLD:
+ return true;
+ case FONT_WEIGHT_NORMAL:
+ case FONT_WEIGHT_UNDEFINED:
+ case UNRECOGNIZED:
+ return false;
+ }
+
+ return false;
+ }
+
+ private static int fontStyleToTypefaceStyle(FontStyle fontStyle) {
+ final boolean isBold = isBold(fontStyle);
+ final boolean isItalic = fontStyle.getItalic().getValue();
+
+ if (isBold && isItalic) {
+ return Typeface.BOLD_ITALIC;
+ } else if (isBold) {
+ return Typeface.BOLD;
+ } else if (isItalic) {
+ return Typeface.ITALIC;
+ } else {
+ return Typeface.NORMAL;
+ }
+ }
+
+ @SuppressWarnings("nullness")
+ private static Typeface createTypeface(
+ FontStyle fontStyle, @Nullable Typeface currentTypeface) {
+ return Typeface.create(currentTypeface, fontStyleToTypefaceStyle(fontStyle));
+ }
+
+ private static MetricAffectingSpan createTypefaceSpan(FontStyle fontStyle) {
+ return new StyleSpan(fontStyleToTypefaceStyle(fontStyle));
+ }
+
+ private static boolean hasDefaultTypeface(FontStyle fontStyle) {
+ return !fontStyle.getItalic().getValue() && !isBold(fontStyle);
+ }
+
+ private static void applyFontStyle(FontStyle style, TextView textView) {
+ Typeface currentTypeface = textView.getTypeface();
+
+ if (!hasDefaultTypeface(style)) {
+ // Need to supply typefaceStyle when creating the typeface (will select specialist
+ // bold/italic typefaces), *and* when setting the typeface (will set synthetic
+ // bold/italic flags in Paint if they're not supported by the given typeface).
+ textView.setTypeface(
+ createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
+ }
+
+ int currentPaintFlags = textView.getPaintFlags();
+
+ // Remove the bits we're setting
+ currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
+
+ if (style.hasUnderline() && style.getUnderline().getValue()) {
+ currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
+ }
+
+ textView.setPaintFlags(currentPaintFlags);
+
+ if (style.hasSize()) {
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, style.getSize().getValue());
+ }
+
+ if (style.hasLetterSpacing()) {
+ textView.setLetterSpacing(style.getLetterSpacing().getValue());
+ }
+
+ textView.setTextColor(extractTextColorArgb(style));
+ }
+
+ private void applyClickable(View view, Clickable clickable) {
+ view.setTag(clickable.getId());
+
+ boolean hasAction = false;
+ switch (clickable.getOnClick().getValueCase()) {
+ case LAUNCH_ACTION:
+ Intent i =
+ buildLaunchActionIntent(
+ clickable.getOnClick().getLaunchAction(), clickable.getId());
+ if (i != null) {
+ hasAction = true;
+ view.setOnClickListener(
+ v -> {
+ if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
+ mAppContext.startActivity(i);
+ }
+ });
+ }
+ break;
+ case LOAD_ACTION:
+ hasAction = true;
+ view.setOnClickListener(
+ v ->
+ mLoadActionListener.onClick(
+ buildState(
+ clickable.getOnClick().getLoadAction(),
+ clickable.getId())));
+ break;
+ case VALUE_NOT_SET:
+ break;
+ }
+
+ if (hasAction) {
+ // Apply ripple effect
+ TypedValue outValue = new TypedValue();
+ mAppContext
+ .getTheme()
+ .resolveAttribute(
+ android.R.attr.selectableItemBackground,
+ outValue,
+ /* resolveRefs= */ true);
+ view.setForeground(mAppContext.getDrawable(outValue.resourceId));
+ }
+ }
+
+ private void applyPadding(View view, Padding padding) {
+ if (padding.getRtlAware().getValue()) {
+ view.setPaddingRelative(
+ safeDpToPx(padding.getStart()),
+ safeDpToPx(padding.getTop()),
+ safeDpToPx(padding.getEnd()),
+ safeDpToPx(padding.getBottom()));
+ } else {
+ view.setPadding(
+ safeDpToPx(padding.getStart()),
+ safeDpToPx(padding.getTop()),
+ safeDpToPx(padding.getEnd()),
+ safeDpToPx(padding.getBottom()));
+ }
+ }
+
+ private GradientDrawable applyBackground(
+ View view, Background background, @Nullable GradientDrawable drawable) {
+ if (drawable == null) {
+ drawable = new GradientDrawable();
+ }
+
+ if (background.hasColor()) {
+ drawable.setColor(background.getColor().getArgb());
+ }
+
+ if (background.hasCorner()) {
+ drawable.setCornerRadius(safeDpToPx(background.getCorner().getRadius()));
+ view.setClipToOutline(true);
+ view.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
+ }
+
+ return drawable;
+ }
+
+ private GradientDrawable applyBorder(Border border, @Nullable GradientDrawable drawable) {
+ if (drawable == null) {
+ drawable = new GradientDrawable();
+ }
+
+ drawable.setStroke(safeDpToPx(border.getWidth()), border.getColor().getArgb());
+
+ return drawable;
+ }
+
+ private View applyModifiers(View view, Modifiers modifiers) {
+ if (modifiers.hasClickable()) {
+ applyClickable(view, modifiers.getClickable());
+ }
+
+ if (modifiers.hasSemantics()) {
+ applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+ }
+
+ if (modifiers.hasPadding()) {
+ applyPadding(view, modifiers.getPadding());
+ }
+
+ GradientDrawable backgroundDrawable = null;
+
+ if (modifiers.hasBackground()) {
+ backgroundDrawable =
+ applyBackground(view, modifiers.getBackground(), backgroundDrawable);
+ }
+
+ if (modifiers.hasBorder()) {
+ backgroundDrawable = applyBorder(modifiers.getBorder(), backgroundDrawable);
+ }
+
+ if (backgroundDrawable != null) {
+ view.setBackground(backgroundDrawable);
+ }
+
+ return view;
+ }
+
+ // This is a little nasty; ArcLayoutWidget is just an interface, so we have no guarantee that
+ // the instance also extends View (as it should). Instead, just take a View in and rename
+ // this, and check that it's an ArcLayoutWidget internally.
+ private View applyModifiersToArcLayoutView(View view, ArcModifiers modifiers) {
+ if (!(view instanceof WearArcLayout.ArcLayoutWidget)) {
+ Log.e(
+ TAG,
+ "applyModifiersToArcLayoutView should only be called with an ArcLayoutWidget");
+ return view;
+ }
+
+ if (modifiers.hasClickable()) {
+ applyClickable(view, modifiers.getClickable());
+ }
+
+ if (modifiers.hasSemantics()) {
+ applyAudibleParams(view, modifiers.getSemantics().getContentDescription());
+ }
+
+ return view;
+ }
+
+ private void applyFontStyle(FontStyle style, WearCurvedTextView textView) {
+ Typeface currentTypeface = textView.getTypeface();
+
+ if (!hasDefaultTypeface(style)) {
+ // Need to supply typefaceStyle when creating the typeface (will select specialist
+ // bold/italic typefaces), *and* when setting the typeface (will set synthetic
+ // bold/italic flags in Paint if they're not supported by the given typeface).
+ textView.setTypeface(
+ createTypeface(style, currentTypeface), fontStyleToTypefaceStyle(style));
+ }
+
+ int currentPaintFlags = textView.getPaintFlags();
+
+ // Remove the bits we're setting
+ currentPaintFlags &= ~Paint.UNDERLINE_TEXT_FLAG;
+
+ if (style.hasUnderline() && style.getUnderline().getValue()) {
+ currentPaintFlags |= Paint.UNDERLINE_TEXT_FLAG;
+ }
+
+ textView.setPaintFlags(currentPaintFlags);
+
+ if (style.hasSize()) {
+ textView.setTextSize(
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ style.getSize().getValue(),
+ mAppContext.getResources().getDisplayMetrics()));
+ }
+ }
+
+ private static int textAlignToAndroidGravity(TextAlignmentProp alignment) {
+ switch (alignment.getValue()) {
+ case TEXT_ALIGN_START:
+ return Gravity.START;
+ case TEXT_ALIGN_CENTER:
+ return Gravity.CENTER_HORIZONTAL;
+ case TEXT_ALIGN_END:
+ return Gravity.END;
+ case TEXT_ALIGN_UNDEFINED:
+ case UNRECOGNIZED:
+ return TEXT_ALIGN_DEFAULT;
+ }
+
+ return TEXT_ALIGN_DEFAULT;
+ }
+
+ @Nullable
+ private static TruncateAt textTruncationToEllipsize(TextOverflowProp type) {
+ switch (type.getValue()) {
+ case TEXT_OVERFLOW_TRUNCATE:
+ // A null TruncateAt disables adding an ellipsis.
+ return null;
+ case TEXT_OVERFLOW_ELLIPSIZE_END:
+ return TruncateAt.END;
+ case TEXT_OVERFLOW_UNDEFINED:
+ case UNRECOGNIZED:
+ return TEXT_OVERFLOW_DEFAULT;
+ }
+
+ return TEXT_OVERFLOW_DEFAULT;
+ }
+
+ @WearArcLayout.AnchorType
+ private static int anchorTypeToAnchorPos(ArcAnchorTypeProp type) {
+ switch (type.getValue()) {
+ case ARC_ANCHOR_START:
+ return WearArcLayout.ANCHOR_START;
+ case ARC_ANCHOR_CENTER:
+ return WearArcLayout.ANCHOR_CENTER;
+ case ARC_ANCHOR_END:
+ return WearArcLayout.ANCHOR_END;
+ case ARC_ANCHOR_UNDEFINED:
+ case UNRECOGNIZED:
+ return ARC_ANCHOR_DEFAULT;
+ }
+
+ return ARC_ANCHOR_DEFAULT;
+ }
+
+ private int dimensionToPx(ContainerDimension containerDimension) {
+ switch (containerDimension.getInnerCase()) {
+ case LINEAR_DIMENSION:
+ return safeDpToPx(containerDimension.getLinearDimension());
+ case EXPANDED_DIMENSION:
+ return LayoutParams.MATCH_PARENT;
+ case WRAPPED_DIMENSION:
+ return LayoutParams.WRAP_CONTENT;
+ case INNER_NOT_SET:
+ return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
+ }
+
+ return dimensionToPx(CONTAINER_DIMENSION_DEFAULT);
+ }
+
+ private static int extractTextColorArgb(FontStyle fontStyle) {
+ if (fontStyle.hasColor()) {
+ return fontStyle.getColor().getArgb();
+ } else {
+ return TEXT_COLOR_DEFAULT;
+ }
+ }
+
+ /**
+ * Returns an Android {@link Intent} that can perform the action defined in the given tile
+ * {@link LaunchAction}.
+ */
+ @Nullable
+ static Intent buildLaunchActionIntent(
+ @NonNull LaunchAction launchAction, @NonNull String clickableId) {
+ if (launchAction.hasAndroidActivity()) {
+ AndroidActivity activity = launchAction.getAndroidActivity();
+ Intent i =
+ new Intent().setClassName(activity.getPackageName(), activity.getClassName());
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ if (!clickableId.isEmpty()) {
+ i.putExtra(TileProviderService.EXTRA_CLICKABLE_ID, clickableId);
+ }
+
+ return i;
+ }
+
+ return null;
+ }
+
+ static State buildState(LoadAction loadAction, String clickableId) {
+ // Get the state specified by the provider and add the last clicked clickable's ID to it.
+ return loadAction.getRequestState().toBuilder().setLastClickableId(clickableId).build();
+ }
+
+ @Nullable
+ private View inflateColumn(ViewGroup parent, Column column) {
+ ContainerDimension width =
+ column.hasWidth() ? column.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+ ContainerDimension height =
+ column.hasHeight() ? column.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+ if (!canMeasureContainer(width, height, column.getContentsList())) {
+ Log.w(TAG, "Column set to wrap but contents are unmeasurable. Ignoring.");
+ return null;
+ }
+
+ LinearLayout linearLayout = new LinearLayout(mAppContext);
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ linearLayout.setGravity(horizontalAlignmentToGravity(column.getHorizontalAlignment()));
+
+ layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+ View wrappedView = applyModifiers(linearLayout, column.getModifiers());
+
+ parent.addView(wrappedView, layoutParams);
+ inflateLayoutElements(linearLayout, column.getContentsList());
+
+ return wrappedView;
+ }
+
+ @Nullable
+ private View inflateRow(ViewGroup parent, Row row) {
+ ContainerDimension width = row.hasWidth() ? row.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+ ContainerDimension height = row.hasHeight() ? row.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+ if (!canMeasureContainer(width, height, row.getContentsList())) {
+ Log.w(TAG, "Row set to wrap but contents are unmeasurable. Ignoring.");
+ return null;
+ }
+
+ LinearLayout linearLayout = new LinearLayout(mAppContext);
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ linearLayout.setGravity(verticalAlignmentToGravity(row.getVerticalAlignment()));
+
+ layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+ View wrappedView = applyModifiers(linearLayout, row.getModifiers());
+
+ parent.addView(wrappedView, layoutParams);
+ inflateLayoutElements(linearLayout, row.getContentsList());
+
+ return wrappedView;
+ }
+
+ @Nullable
+ private View inflateBox(ViewGroup parent, Box box) {
+ ContainerDimension width = box.hasWidth() ? box.getWidth() : CONTAINER_DIMENSION_DEFAULT;
+ ContainerDimension height = box.hasHeight() ? box.getHeight() : CONTAINER_DIMENSION_DEFAULT;
+
+ if (!canMeasureContainer(width, height, box.getContentsList())) {
+ Log.w(TAG, "Box set to wrap but contents are unmeasurable. Ignoring.");
+ return null;
+ }
+
+ FrameLayout frame = new FrameLayout(mAppContext);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ layoutParams = updateLayoutParams(parent, layoutParams, width, height);
+
+ int gravity =
+ horizontalAlignmentToGravity(box.getHorizontalAlignment())
+ | verticalAlignmentToGravity(box.getVerticalAlignment());
+
+ View wrappedView = applyModifiers(frame, box.getModifiers());
+
+ parent.addView(wrappedView, layoutParams);
+ inflateLayoutElements(frame, box.getContentsList());
+
+ // We can't set layout gravity to a FrameLayout ahead of time (and foregroundGravity only
+ // sets the gravity of the foreground Drawable). Go and apply gravity to the child.
+ applyGravityToFrameLayoutChildren(frame, gravity);
+
+ return wrappedView;
+ }
+
+ @Nullable
+ private View inflateSpacer(ViewGroup parent, Spacer spacer) {
+ int widthPx = safeDpToPx(spacer.getWidth().getLinearDimension());
+ int heightPx = safeDpToPx(spacer.getHeight().getLinearDimension());
+
+ if (widthPx == 0 && heightPx == 0) {
+ return null;
+ }
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ // Modifiers cannot be applied to android's Space, so use a plain View if this Spacer has
+ // modifiers.
+ View view;
+ if (spacer.hasModifiers()) {
+ view = applyModifiers(new View(mAppContext), spacer.getModifiers());
+ layoutParams =
+ updateLayoutParams(
+ parent,
+ layoutParams,
+ spacerDimensionToContainerDimension(spacer.getWidth()),
+ spacerDimensionToContainerDimension(spacer.getHeight()));
+ } else {
+ view = new Space(mAppContext);
+ view.setMinimumWidth(widthPx);
+ view.setMinimumHeight(heightPx);
+ }
+
+ parent.addView(view, layoutParams);
+
+ return view;
+ }
+
+ @Nullable
+ private View inflateArcSpacer(ViewGroup parent, ArcSpacer spacer) {
+ float lengthDegrees = max(0, spacer.getLength().getValue());
+ int thicknessPx = safeDpToPx(spacer.getThickness());
+
+ if (lengthDegrees == 0 && thicknessPx == 0) {
+ return null;
+ }
+
+ WearCurvedSpacer space = new WearCurvedSpacer(mAppContext);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ space.setSweepAngleDegrees(lengthDegrees);
+ space.setThicknessPx(thicknessPx);
+
+ View wrappedView = applyModifiersToArcLayoutView(space, spacer.getModifiers());
+ parent.addView(wrappedView, layoutParams);
+
+ return wrappedView;
+ }
+
+ private View inflateText(ViewGroup parent, Text text) {
+ TextView textView =
+ new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ textView.setText(text.getText().getValue());
+
+ textView.setEllipsize(textTruncationToEllipsize(text.getOverflow()));
+ textView.setGravity(textAlignToAndroidGravity(text.getMultilineAlignment()));
+
+ if (text.hasMaxLines()) {
+ textView.setMaxLines(max(TEXT_MIN_LINES, text.getMaxLines().getValue()));
+ } else {
+ textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
+ }
+
+ // Setting colours **must** go after setting the Text Appearance, otherwise it will get
+ // immediately overridden.
+ if (text.hasFontStyle()) {
+ applyFontStyle(text.getFontStyle(), textView);
+ } else {
+ applyFontStyle(FontStyle.getDefaultInstance(), textView);
+ }
+
+ if (text.hasLineHeight()) {
+ float lineHeight =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ text.getLineHeight().getValue(),
+ mAppContext.getResources().getDisplayMetrics());
+ final float fontHeight = textView.getPaint().getFontSpacing();
+ if (lineHeight != fontHeight) {
+ textView.setLineSpacing(lineHeight - fontHeight, 1f);
+ }
+ }
+
+ View wrappedView = applyModifiers(textView, text.getModifiers());
+ parent.addView(wrappedView, layoutParams);
+
+ // We don't want the text to be screen-reader focusable, unless wrapped in a Audible. This
+ // prevents automatically reading out partial text (e.g. text in a row) etc.
+ textView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+
+ return wrappedView;
+ }
+
+ private View inflateArcText(ViewGroup parent, ArcText text) {
+ WearCurvedTextView textView =
+ new WearCurvedTextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = LayoutParams.MATCH_PARENT;
+
+ textView.setText(text.getText().getValue());
+
+ if (text.hasFontStyle()) {
+ applyFontStyle(text.getFontStyle(), textView);
+ }
+
+ textView.setTextColor(extractTextColorArgb(text.getFontStyle()));
+
+ View wrappedView = applyModifiersToArcLayoutView(textView, text.getModifiers());
+ parent.addView(wrappedView, layoutParams);
+
+ return wrappedView;
+ }
+
+ private static boolean isZeroLengthImageDimension(ImageDimension dimension) {
+ return dimension.getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION
+ && dimension.getLinearDimension().getValue() == 0;
+ }
+
+ private static ContainerDimension imageDimensionToContainerDimension(ImageDimension dimension) {
+ switch (dimension.getInnerCase()) {
+ case LINEAR_DIMENSION:
+ return ContainerDimension.newBuilder()
+ .setLinearDimension(dimension.getLinearDimension())
+ .build();
+ case EXPANDED_DIMENSION:
+ return ContainerDimension.newBuilder()
+ .setExpandedDimension(ExpandedDimensionProp.getDefaultInstance())
+ .build();
+ case PROPORTIONAL_DIMENSION:
+ // A ratio size should be translated to a WRAP_CONTENT; the RatioViewWrapper will
+ // deal with the sizing of that.
+ return ContainerDimension.newBuilder()
+ .setWrappedDimension(WrappedDimensionProp.getDefaultInstance())
+ .build();
+ case INNER_NOT_SET:
+ break;
+ }
+ // Caller should have already checked for this.
+ throw new IllegalArgumentException(
+ "ImageDimension has an unknown dimension type: " + dimension.getInnerCase().name());
+ }
+
+ private static ContainerDimension spacerDimensionToContainerDimension(
+ SpacerDimension dimension) {
+ switch (dimension.getInnerCase()) {
+ case LINEAR_DIMENSION:
+ return ContainerDimension.newBuilder()
+ .setLinearDimension(dimension.getLinearDimension())
+ .build();
+ case INNER_NOT_SET:
+ // A spacer is allowed to have missing dimension and this should be considered as
+ // 0dp.
+ return ContainerDimension.newBuilder()
+ .setLinearDimension(DpProp.getDefaultInstance())
+ .build();
+ }
+ // Caller should have already checked for this.
+ throw new IllegalArgumentException(
+ "SpacerDimension has an unknown dimension type: "
+ + dimension.getInnerCase().name());
+ }
+
+ @SuppressWarnings("ExecutorTaskName")
+ @Nullable
+ private View inflateImage(ViewGroup parent, Image image) {
+ String protoResId = image.getResourceId().getValue();
+
+ // If either width or height isn't set, abort.
+ if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET
+ || image.getHeight().getInnerCase() == ImageDimension.InnerCase.INNER_NOT_SET) {
+ Log.w(TAG, "One of width and height not set on image " + protoResId);
+ return null;
+ }
+
+ // The image must occupy _some_ space.
+ if (isZeroLengthImageDimension(image.getWidth())
+ || isZeroLengthImageDimension(image.getHeight())) {
+ Log.w(TAG, "One of width and height was zero on image " + protoResId);
+ return null;
+ }
+
+ // Both dimensions can't be ratios.
+ if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION
+ && image.getHeight().getInnerCase()
+ == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+ Log.w(TAG, "Both width and height were proportional for image " + protoResId);
+ return null;
+ }
+
+ // Pull the ratio for the RatioViewWrapper. Was either argument a proportional dimension?
+ @Nullable Float ratio = RatioViewWrapper.UNDEFINED_ASPECT_RATIO;
+
+ if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+ ratio = safeAspectRatioOrNull(image.getWidth().getProportionalDimension());
+ }
+
+ if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.PROPORTIONAL_DIMENSION) {
+ ratio = safeAspectRatioOrNull(image.getHeight().getProportionalDimension());
+ }
+
+ if (ratio == null) {
+ Log.w(TAG, "Invalid aspect ratio for image " + protoResId);
+ return null;
+ }
+
+ ImageView imageView = new ImageView(mAppContext);
+
+ if (image.hasContentScaleMode()) {
+ imageView.setScaleType(
+ contentScaleModeToScaleType(image.getContentScaleMode().getValue()));
+ }
+
+ if (image.getWidth().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
+ imageView.setMinimumWidth(safeDpToPx(image.getWidth().getLinearDimension()));
+ }
+
+ if (image.getHeight().getInnerCase() == ImageDimension.InnerCase.LINEAR_DIMENSION) {
+ imageView.setMinimumHeight(safeDpToPx(image.getHeight().getLinearDimension()));
+ }
+
+ // We need to sort out the sizing of the widget now, so we can pass the correct params to
+ // RatioViewWrapper. First, translate the ImageSize to a ContainerSize. A ratio size should
+ // be translated to a WRAP_CONTENT; the RatioViewWrapper will deal with the sizing of that.
+ LayoutParams ratioWrapperLayoutParams = generateDefaultLayoutParams();
+ ratioWrapperLayoutParams =
+ updateLayoutParams(
+ parent,
+ ratioWrapperLayoutParams,
+ imageDimensionToContainerDimension(image.getWidth()),
+ imageDimensionToContainerDimension(image.getHeight()));
+
+ RatioViewWrapper ratioViewWrapper = new RatioViewWrapper(mAppContext);
+ ratioViewWrapper.setAspectRatio(ratio);
+ ratioViewWrapper.addView(imageView);
+
+ // Finally, wrap the image in any modifiers...
+ View wrappedView = applyModifiers(ratioViewWrapper, image.getModifiers());
+
+ parent.addView(wrappedView, ratioWrapperLayoutParams);
+
+ ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
+ if (drawableFuture.isDone()) {
+ // If the future is done, immediately draw.
+ setImageDrawable(imageView, drawableFuture, protoResId);
+ } else {
+ // Otherwise, handle the result on the UI thread.
+ drawableFuture.addListener(
+ () -> setImageDrawable(imageView, drawableFuture, protoResId),
+ ContextCompat.getMainExecutor(mAppContext));
+ }
+
+ return wrappedView;
+ }
+
+ private static void setImageDrawable(
+ ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
+ try {
+ imageView.setImageDrawable(drawableFuture.get());
+ } catch (ExecutionException | InterruptedException e) {
+ Log.w(TAG, "Could not get drawable for image " + protoResId);
+ }
+ }
+
+ @Nullable
+ private View inflateArcLine(ViewGroup parent, ArcLine line) {
+ float lengthDegrees = max(0, line.getLength().getValue());
+ int thicknessPx = safeDpToPx(line.getThickness());
+
+ if (lengthDegrees == 0 && thicknessPx == 0) {
+ return null;
+ }
+
+ WearCurvedLineView lineView = new WearCurvedLineView(mAppContext);
+
+ // A ArcLineView must always be the same width/height as its parent, so it can draw the line
+ // properly inside of those bounds.
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = LayoutParams.MATCH_PARENT;
+
+ int lineColor = LINE_COLOR_DEFAULT;
+ if (line.hasColor()) {
+ lineColor = line.getColor().getArgb();
+ }
+
+ lineView.setThicknessPx(thicknessPx);
+ lineView.setSweepAngleDegrees(lengthDegrees);
+ lineView.setColor(lineColor);
+
+ View wrappedView = applyModifiersToArcLayoutView(lineView, line.getModifiers());
+ parent.addView(wrappedView, layoutParams);
+
+ return wrappedView;
+ }
+
+ @Nullable
+ private View inflateArc(ViewGroup parent, Arc arc) {
+ WearArcLayout arcLayout = new WearArcLayout(mAppContext);
+
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+ layoutParams.width = LayoutParams.MATCH_PARENT;
+ layoutParams.height = LayoutParams.MATCH_PARENT;
+
+ arcLayout.setAnchorAngleDegrees(arc.getAnchorAngle().getValue());
+ arcLayout.setAnchorType(anchorTypeToAnchorPos(arc.getAnchorType()));
+
+ // Add all children.
+ for (ArcLayoutElement child : arc.getContentsList()) {
+ @Nullable View childView = inflateArcLayoutElement(arcLayout, child);
+ if (childView != null) {
+ WearArcLayout.LayoutParams childLayoutParams =
+ (WearArcLayout.LayoutParams) childView.getLayoutParams();
+ boolean rotate = false;
+ if (child.hasAdapter()) {
+ rotate = child.getAdapter().getRotateContents().getValue();
+ }
+
+ // Apply rotation and gravity.
+ childLayoutParams.setRotate(rotate);
+ childLayoutParams.setVerticalAlignment(
+ verticalAlignmentToArcVAlign(arc.getVerticalAlign()));
+ }
+ }
+
+ View wrappedView = applyModifiers(arcLayout, arc.getModifiers());
+ parent.addView(wrappedView, layoutParams);
+
+ return wrappedView;
+ }
+
+ private void applyStylesToSpan(
+ SpannableStringBuilder builder, int start, int end, FontStyle fontStyle) {
+ if (fontStyle.hasSize()) {
+ float fontSize =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ fontStyle.getSize().getValue(),
+ mAppContext.getResources().getDisplayMetrics());
+
+ AbsoluteSizeSpan span = new AbsoluteSizeSpan(round(fontSize));
+ builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+ }
+
+ if (!hasDefaultTypeface(fontStyle)) {
+ MetricAffectingSpan span = createTypefaceSpan(fontStyle);
+ builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+ }
+
+ if (fontStyle.getUnderline().getValue()) {
+ UnderlineSpan span = new UnderlineSpan();
+ builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+ }
+
+ if (fontStyle.hasLetterSpacing()) {
+ LetterSpacingSpan span = new LetterSpacingSpan(fontStyle.getLetterSpacing().getValue());
+ builder.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
+ }
+
+ ForegroundColorSpan colorSpan;
+
+ colorSpan = new ForegroundColorSpan(extractTextColorArgb(fontStyle));
+
+ builder.setSpan(colorSpan, start, end, Spanned.SPAN_MARK_MARK);
+ }
+
+ private void applyModifiersToSpan(
+ SpannableStringBuilder builder, int start, int end, SpanModifiers modifiers) {
+ if (modifiers.hasClickable()) {
+ ClickableSpan clickableSpan = new TilesClickableSpan(modifiers.getClickable());
+
+ builder.setSpan(clickableSpan, start, end, Spanned.SPAN_MARK_MARK);
+ }
+ }
+
+ private SpannableStringBuilder inflateTextInSpannable(
+ SpannableStringBuilder builder, SpanText text) {
+ int currentPos = builder.length();
+ int lastPos = currentPos + text.getText().getValue().length();
+
+ builder.append(text.getText().getValue());
+
+ applyStylesToSpan(builder, currentPos, lastPos, text.getFontStyle());
+ applyModifiersToSpan(builder, currentPos, lastPos, text.getModifiers());
+
+ return builder;
+ }
+
+ @SuppressWarnings("ExecutorTaskName")
+ private SpannableStringBuilder inflateImageInSpannable(
+ SpannableStringBuilder builder, SpanImage protoImage, TextView textView) {
+ String protoResId = protoImage.getResourceId().getValue();
+
+ if (protoImage.getWidth().getValue() == 0 || protoImage.getHeight().getValue() == 0) {
+ Log.w(TAG, "One of width and height was zero on image " + protoResId);
+ return builder;
+ }
+
+ ListenableFuture<Drawable> drawableFuture = mResourceAccessors.getDrawable(protoResId);
+ if (drawableFuture.isDone()) {
+ // If the future is done, immediately add drawable to builder.
+ try {
+ Drawable drawable = drawableFuture.get();
+ appendSpanDrawable(builder, drawable, protoImage);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.w(
+ TAG,
+ "Could not get drawable for image "
+ + protoImage.getResourceId().getValue());
+ }
+ } else {
+ // If the future is not done, add an empty drawable to builder as a placeholder.
+ Drawable emptyDrawable = new ColorDrawable(Color.TRANSPARENT);
+ int startInclusive = builder.length();
+ ImageSpan emptyDrawableSpan = appendSpanDrawable(builder, emptyDrawable, protoImage);
+ int endExclusive = builder.length();
+
+ // When the future is done, replace the empty drawable with the received one.
+ drawableFuture.addListener(
+ () -> {
+ // Remove the placeholder. This should be safe, even with other modifiers
+ // applied. This just removes the single drawable span, and should leave
+ // other spans in place.
+ builder.removeSpan(emptyDrawableSpan);
+ // Add the new drawable to the same range.
+ setSpanDrawable(
+ builder, drawableFuture, startInclusive, endExclusive, protoImage);
+ // Update the TextView.
+ textView.setText(builder);
+ },
+ ContextCompat.getMainExecutor(mAppContext));
+ }
+
+ return builder;
+ }
+
+ private ImageSpan appendSpanDrawable(
+ SpannableStringBuilder builder, Drawable drawable, SpanImage protoImage) {
+ drawable.setBounds(
+ 0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
+ ImageSpan imgSpan = new ImageSpan(drawable);
+
+ int startPos = builder.length();
+ builder.append(" ", imgSpan, Spanned.SPAN_MARK_MARK);
+ int endPos = builder.length();
+
+ applyModifiersToSpan(builder, startPos, endPos, protoImage.getModifiers());
+
+ return imgSpan;
+ }
+
+ private void setSpanDrawable(
+ SpannableStringBuilder builder,
+ ListenableFuture<Drawable> drawableFuture,
+ int startInclusive,
+ int endExclusive,
+ SpanImage protoImage) {
+ final String protoResourceId = protoImage.getResourceId().getValue();
+
+ try {
+ // Add the image span to the same range occupied by the placeholder.
+ Drawable drawable = drawableFuture.get();
+ drawable.setBounds(
+ 0, 0, safeDpToPx(protoImage.getWidth()), safeDpToPx(protoImage.getHeight()));
+ ImageSpan imgSpan = new ImageSpan(drawable);
+ builder.setSpan(
+ imgSpan,
+ startInclusive,
+ endExclusive,
+ android.text.Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.w(TAG, "Could not get drawable for image " + protoResourceId);
+ }
+ }
+
+ private View inflateSpannable(ViewGroup parent, Spannable spannable) {
+ TextView tv = new TextView(mAppContext, /* attrs= */ null, R.attr.tilesTextAppearance);
+ LayoutParams layoutParams = generateDefaultLayoutParams();
+
+ SpannableStringBuilder builder = new SpannableStringBuilder();
+
+ for (Span element : spannable.getSpansList()) {
+ switch (element.getInnerCase()) {
+ case IMAGE:
+ SpanImage protoImage = element.getImage();
+ builder = inflateImageInSpannable(builder, protoImage, tv);
+ break;
+ case TEXT:
+ SpanText protoText = element.getText();
+ builder = inflateTextInSpannable(builder, protoText);
+ break;
+ default:
+ Log.w(TAG, "Unknown Span child type.");
+ break;
+ }
+ }
+
+ tv.setEllipsize(textTruncationToEllipsize(spannable.getOverflow()));
+ tv.setGravity(horizontalAlignmentToGravity(spannable.getMultilineAlignment()));
+
+ if (spannable.hasMaxLines()) {
+ tv.setMaxLines(max(TEXT_MIN_LINES, spannable.getMaxLines().getValue()));
+ } else {
+ tv.setMaxLines(TEXT_MAX_LINES_DEFAULT);
+ }
+
+ if (spannable.hasLineSpacing()) {
+ float lineSpacing =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ spannable.getLineSpacing().getValue(),
+ mAppContext.getResources().getDisplayMetrics());
+ tv.setLineSpacing(lineSpacing, 1f);
+ }
+
+ tv.setText(builder);
+
+ View wrappedView = applyModifiers(tv, spannable.getModifiers());
+ parent.addView(applyModifiers(tv, spannable.getModifiers()), layoutParams);
+
+ return wrappedView;
+ }
+
+ @Nullable
+ private View inflateArcLayoutElement(ViewGroup parent, ArcLayoutElement element) {
+ View inflatedView = null;
+
+ switch (element.getInnerCase()) {
+ case ADAPTER:
+ // Fall back to the normal inflater.
+ inflatedView = inflateLayoutElement(parent, element.getAdapter().getContent());
+ break;
+
+ case SPACER:
+ inflatedView = inflateArcSpacer(parent, element.getSpacer());
+ break;
+
+ case LINE:
+ inflatedView = inflateArcLine(parent, element.getLine());
+ break;
+
+ case TEXT:
+ inflatedView = inflateArcText(parent, element.getText());
+ break;
+
+ case INNER_NOT_SET:
+ break;
+ }
+
+ if (inflatedView == null) {
+ // Covers null (returned when the childCase in the proto isn't known). Sadly, ProtoLite
+ // doesn't give us a way to access childCase's underlying tag, so we can't give any
+ // smarter error message here.
+ Log.w(TAG, "Unknown child type");
+ }
+
+ return inflatedView;
+ }
+
+ @Nullable
+ private View inflateLayoutElement(ViewGroup parent, LayoutElement element) {
+ // What is it?
+ View inflatedView = null;
+ switch (element.getInnerCase()) {
+ case COLUMN:
+ inflatedView = inflateColumn(parent, element.getColumn());
+ break;
+ case ROW:
+ inflatedView = inflateRow(parent, element.getRow());
+ break;
+ case BOX:
+ inflatedView = inflateBox(parent, element.getBox());
+ break;
+ case SPACER:
+ inflatedView = inflateSpacer(parent, element.getSpacer());
+ break;
+ case TEXT:
+ inflatedView = inflateText(parent, element.getText());
+ break;
+ case IMAGE:
+ inflatedView = inflateImage(parent, element.getImage());
+ break;
+ case ARC:
+ inflatedView = inflateArc(parent, element.getArc());
+ break;
+ case SPANNABLE:
+ inflatedView = inflateSpannable(parent, element.getSpannable());
+ break;
+ case INNER_NOT_SET:
+ default: // TODO(b/178359365): Remove default case
+ Log.w(TAG, "Unknown child type: " + element.getInnerCase().name());
+ break;
+ }
+
+ return inflatedView;
+ }
+
+ private boolean canMeasureContainer(
+ ContainerDimension containerWidth,
+ ContainerDimension containerHeight,
+ List<LayoutElement> elements) {
+ // We can't measure a container if it's set to wrap-contents but all of its contents are set
+ // to expand-to-parent. Such containers must not be displayed.
+ if (containerWidth.hasWrappedDimension() && !containsMeasurableWidth(elements)) {
+ return false;
+ }
+ if (containerHeight.hasWrappedDimension() && !containsMeasurableHeight(elements)) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean containsMeasurableWidth(List<LayoutElement> elements) {
+ for (LayoutElement element : elements) {
+ if (isWidthMeasurable(element)) {
+ // Enough to find a single element that is measurable.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean containsMeasurableHeight(List<LayoutElement> elements) {
+ for (LayoutElement element : elements) {
+ if (isHeightMeasurable(element)) {
+ // Enough to find a single element that is measurable.
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isWidthMeasurable(LayoutElement element) {
+ switch (element.getInnerCase()) {
+ case COLUMN:
+ return isMeasurable(element.getColumn().getWidth());
+ case ROW:
+ return isMeasurable(element.getRow().getWidth());
+ case BOX:
+ return isMeasurable(element.getBox().getWidth());
+ case SPACER:
+ return isMeasurable(element.getSpacer().getWidth());
+ case IMAGE:
+ return isMeasurable(element.getImage().getWidth());
+ case ARC:
+ case TEXT:
+ case SPANNABLE:
+ return true;
+ case INNER_NOT_SET:
+ default: // TODO(b/178359365): Remove default case
+ return false;
+ }
+ }
+
+ private boolean isHeightMeasurable(LayoutElement element) {
+ switch (element.getInnerCase()) {
+ case COLUMN:
+ return isMeasurable(element.getColumn().getHeight());
+ case ROW:
+ return isMeasurable(element.getRow().getHeight());
+ case BOX:
+ return isMeasurable(element.getBox().getHeight());
+ case SPACER:
+ return isMeasurable(element.getSpacer().getHeight());
+ case IMAGE:
+ return isMeasurable(element.getImage().getHeight());
+ case ARC:
+ case TEXT:
+ case SPANNABLE:
+ return true;
+ case INNER_NOT_SET:
+ default: // TODO(b/178359365): Remove default case
+ return false;
+ }
+ }
+
+ private boolean isMeasurable(ContainerDimension dimension) {
+ return dimensionToPx(dimension) != LayoutParams.MATCH_PARENT;
+ }
+
+ private static boolean isMeasurable(ImageDimension dimension) {
+ switch (dimension.getInnerCase()) {
+ case LINEAR_DIMENSION:
+ case PROPORTIONAL_DIMENSION:
+ return true;
+ case EXPANDED_DIMENSION:
+ case INNER_NOT_SET:
+ return false;
+ }
+ return false;
+ }
+
+ private static boolean isMeasurable(SpacerDimension dimension) {
+ switch (dimension.getInnerCase()) {
+ case LINEAR_DIMENSION:
+ return true;
+ case INNER_NOT_SET:
+ return false;
+ }
+ return false;
+ }
+
+ private void inflateLayoutElements(ViewGroup parent, List<LayoutElement> elements) {
+ for (LayoutElement element : elements) {
+ inflateLayoutElement(parent, element);
+ }
+ }
+
+ /**
+ * Inflates a Tile into {@code parent}.
+ *
+ * @param parent The view to attach the tile into.
+ * @return The first child that was inflated. This may be null if the proto is empty the
+ * top-level LayoutElement has no inner set, or the top-level LayoutElement contains an
+ * unsupported inner type.
+ */
+ @Nullable
+ public View inflate(@NonNull ViewGroup parent) {
+ // Go!
+ return inflateLayoutElement(parent, mLayout.getRoot());
+ }
+
+ private static void applyGravityToFrameLayoutChildren(FrameLayout parent, int gravity) {
+ for (int i = 0; i < parent.getChildCount(); i++) {
+ View child = parent.getChildAt(i);
+
+ // All children should have a LayoutParams already set...
+ if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) {
+ // This...shouldn't happen.
+ throw new IllegalStateException(
+ "Layout params of child is not a descendant of FrameLayout.LayoutParams.");
+ }
+
+ // Children should grow out from the middle of the layout.
+ ((FrameLayout.LayoutParams) child.getLayoutParams()).gravity = gravity;
+ }
+ }
+
+ private static void applyAudibleParams(View view, String accessibilityLabel) {
+ view.setContentDescription(accessibilityLabel);
+ view.setFocusable(true);
+ view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ /** Implementation of ClickableSpan for Tiles' Clickables. */
+ private class TilesClickableSpan extends ClickableSpan {
+ private final Clickable mClickable;
+
+ TilesClickableSpan(Clickable clickable) {
+ this.mClickable = clickable;
+ }
+
+ @Override
+ public void onClick(@NonNull View widget) {
+ Action action = mClickable.getOnClick();
+
+ switch (action.getValueCase()) {
+ case LAUNCH_ACTION:
+ Intent i =
+ buildLaunchActionIntent(action.getLaunchAction(), mClickable.getId());
+ if (i != null) {
+ if (i.resolveActivity(mAppContext.getPackageManager()) != null) {
+ mAppContext.startActivity(i);
+ }
+ }
+ break;
+ case LOAD_ACTION:
+ mLoadActionExecutor.execute(
+ () ->
+ mLoadActionListener.onClick(
+ buildState(
+ action.getLoadAction(), mClickable.getId())));
+
+ break;
+ case VALUE_NOT_SET:
+ break;
+ }
+ }
+
+ @Override
+ public void updateDrawState(@NonNull TextPaint ds) {
+ // Don't change the underlying text appearance.
+ }
+ }
+}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
similarity index 99%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
index 303e12f..5173a09 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearArcLayout.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearArcLayout.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import static java.lang.Math.asin;
import static java.lang.Math.max;
@@ -36,6 +36,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.UiThread;
+import androidx.wear.tiles.renderer.R;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -70,13 +71,10 @@
*
* <p>An example of a widget which implements this interface is {@link WearCurvedTextView}, which
* will lay itself out along the arc.
- *
- * @hide
*/
// TODO(b/174649543): Replace this with the actual androidx.wear.widget.WearArcLayout when
// we've reconciled the two.
@UiThread
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class WearArcLayout extends ViewGroup {
/**
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
similarity index 97%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
index 7a3fe9f..10a2fbb 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedLineView.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedLineView.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import static java.lang.Math.min;
@@ -32,7 +32,7 @@
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
/**
* A line, drawn inside an arc.
@@ -40,10 +40,7 @@
* <p>This widget takes three parameters, the thickness of the line to draw, its sweep angle, and
* the color to draw with. This widget will then draw an arc, with the specified thickness, around
* its parent arc. The sweep angle is specified in degrees, clockwise.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class WearCurvedLineView extends View implements WearArcLayout.ArcLayoutWidget {
private static final int DEFAULT_THICKNESS_PX = 0;
private static final float DEFAULT_SWEEP_ANGLE_DEGREES = 0;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
similarity index 95%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
index 16e937d2..dd29df2 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedSpacer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedSpacer.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import android.content.Context;
import android.content.res.TypedArray;
@@ -23,16 +23,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
/**
* A lightweight curved widget that represents space between elements inside an Arc. This does no
* rendering; it simply causes the parent {@link WearArcLayout} to advance by {@code
* sweepAngleDegrees}.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
public class WearCurvedSpacer extends View implements WearArcLayout.ArcLayoutWidget {
private static final float DEFAULT_SWEEP_ANGLE_DEGREES = 0f;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
similarity index 99%
rename from wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
index 35c0d42..da1d4b8 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/WearCurvedTextView.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/WearCurvedTextView.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.wear.tiles.renderer;
+package androidx.wear.tiles.renderer.internal;
import static java.lang.Math.cos;
import static java.lang.Math.max;
@@ -45,16 +45,13 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
+import androidx.wear.tiles.renderer.R;
/**
* A WearCurvedTextView is a component allowing developers to easily write curved text following the
* curvature of the largest circle that can be inscribed in the view. WearArcLayout could be used to
* concatenate multiple curved texts, also layout together with other widgets such as icons.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY)
// TODO(b/174649543): Replace this with the actual androidx.wear.widget.WearCurvedTextView when
// we've reconciled the two.
public final class WearCurvedTextView extends View implements WearArcLayout.ArcLayoutWidget {
diff --git a/work/workmanager/src/main/java/androidx/work/impl/package-info.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
similarity index 77%
copy from work/workmanager/src/main/java/androidx/work/impl/package-info.java
copy to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
index 3f9ab0d..d5db66f 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/package-info.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/internal/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 The Android Open Source Project
+ * 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.
@@ -15,9 +15,11 @@
*/
/**
+ * Internal implementation for Wear Tiles.
+ *
* @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.work.impl;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.wear.tiles.renderer.internal;
import androidx.annotation.RestrictTo;
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
index 16fe3b2..829af88 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineCache.java
@@ -20,19 +20,18 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.wear.tiles.builders.TimelineBuilders;
-import androidx.wear.tiles.proto.TimelineProto.TimeInterval;
-import androidx.wear.tiles.proto.TimelineProto.Timeline;
import androidx.wear.tiles.proto.TimelineProto.TimelineEntry;
+import androidx.wear.tiles.timeline.internal.TilesTimelineCacheInternal;
/**
* Timeline cache for Wear Tiles. This will take in a full timeline, and return the appropriate
* entry for the given time from {@code findTimelineEntryForTime}.
*/
public final class TilesTimelineCache {
- private final Timeline mTimeline;
+ private final TilesTimelineCacheInternal mCache;
- public TilesTimelineCache(@NonNull TimelineBuilders.Timeline tile) {
- this.mTimeline = tile.toProto();
+ public TilesTimelineCache(@NonNull TimelineBuilders.Timeline timeline) {
+ mCache = new TilesTimelineCacheInternal(timeline.toProto());
}
/**
@@ -48,41 +47,13 @@
@MainThread
@Nullable
public TimelineBuilders.TimelineEntry findTimelineEntryForTime(long timeMillis) {
- TimelineEntry currentEntry = null;
- long currentEntryLength = Long.MAX_VALUE;
+ TimelineEntry entry = mCache.findTimelineEntryForTime(timeMillis);
- // Iterate through, finding the _shortest_ valid timeline entry.
- for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
- if (!entry.hasValidity()) {
- // Only override a default if there's no more specific entry found.
- if (currentEntryLength == Long.MAX_VALUE) {
- // Let's treat an entry with no validity as being a "default", as long as we
- // haven't found
- // any other valid entries
- currentEntry = entry;
- }
- } else {
- TimeInterval validity = entry.getValidity();
-
- long validityLength = validity.getEndMillis() - validity.getStartMillis();
-
- if (validityLength > currentEntryLength) {
- continue;
- }
-
- if (validity.getStartMillis() <= timeMillis
- && timeMillis < validity.getEndMillis()) {
- currentEntry = entry;
- currentEntryLength = validityLength;
- }
- }
- }
-
- if (currentEntry == null) {
+ if (entry == null) {
return null;
}
- return TimelineBuilders.TimelineEntry.fromProto(currentEntry);
+ return TimelineBuilders.TimelineEntry.fromProto(entry);
}
/**
@@ -101,47 +72,13 @@
@MainThread
@Nullable
public TimelineBuilders.TimelineEntry findClosestTimelineEntry(long timeMillis) {
- long currentEntryError = Long.MAX_VALUE;
- TimelineEntry currentEntry = null;
+ TimelineEntry entry = mCache.findClosestTimelineEntry(timeMillis);
- for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
- if (!entry.hasValidity()) {
- // It's a default. This shouldn't happen if we've been called. Skip it.
- continue;
- }
-
- TimeInterval validity = entry.getValidity();
-
- if (!isTimeIntervalValid(validity)) {
- continue;
- }
-
- // It's valid in this time interval. Shouldn't happen. Skip anyway.
- if (validity.getStartMillis() <= timeMillis && timeMillis < validity.getEndMillis()) {
- continue;
- }
-
- long error;
-
- // It's in the future.
- if (validity.getStartMillis() > timeMillis) {
- error = validity.getStartMillis() - timeMillis;
- } else {
- // It's in the past.
- error = timeMillis - validity.getEndMillis();
- }
-
- if (error < currentEntryError) {
- currentEntry = entry;
- currentEntryError = error;
- }
- }
-
- if (currentEntry == null) {
+ if (entry == null) {
return null;
}
- return TimelineBuilders.TimelineEntry.fromProto(currentEntry);
+ return TimelineBuilders.TimelineEntry.fromProto(entry);
}
/**
@@ -157,91 +94,6 @@
*/
public long findCurrentTimelineEntryExpiry(
@NonNull TimelineBuilders.TimelineEntry entry, long fromTimeMillis) {
- long currentSmallestExpiry = Long.MAX_VALUE;
- long entryValidityLength = Long.MAX_VALUE;
-
- TimelineEntry protoEntry = entry.toProto();
-
- if (protoEntry.hasValidity() && protoEntry.getValidity().getEndMillis() > fromTimeMillis) {
- currentSmallestExpiry = protoEntry.getValidity().getEndMillis();
- entryValidityLength =
- protoEntry.getValidity().getEndMillis()
- - protoEntry.getValidity().getStartMillis();
- }
-
- // Search for the starting edge of an overlapping period (i.e. one with startTime between
- // entry.startTime and entry.endTime), with a validity period shorter than the one currently
- // being considered.
- for (TimelineEntry nextEntry : mTimeline.getTimelineEntriesList()) {
- // The entry can't invalidate itself
- if (nextEntry.equals(entry.toProto())) {
- continue;
- }
-
- // Discard if nextEntry doesn't have a validity period. In this case, it's a default (so
- // would potentially be used at entry.end_millis anyway).
- if (!nextEntry.hasValidity()) {
- continue;
- }
-
- TimeInterval nextEntryValidity = nextEntry.getValidity();
-
- // Discard if the validity period is flat out invalid.
- if (!isTimeIntervalValid(nextEntryValidity)) {
- continue;
- }
-
- // Discard if the start time of nextEntry doesn't fall in the current period (it can't
- // interrupt this entry, so this entry's expiry should be used).
- if (protoEntry.hasValidity()) {
- if (nextEntryValidity.getStartMillis() > protoEntry.getValidity().getEndMillis()
- || nextEntryValidity.getStartMillis()
- < protoEntry.getValidity().getStartMillis()) {
- continue;
- }
- }
-
- // Discard if its start time is greater than the current smallest one we've found. In
- // that
- // case, the entry that gave us currentSmallestExpiry would be shown next.
- if (nextEntryValidity.getStartMillis() > currentSmallestExpiry) {
- continue;
- }
-
- // Discard if it's less than "fromTime". This prevents accidentally returning valid
- // times in
- // the past.
- if (nextEntryValidity.getStartMillis() < fromTimeMillis) {
- continue;
- }
-
- // Finally, consider whether the length of the validity period is shorter than the
- // current
- // one. If this doesn't hold, the current entry would be shown instead (the timeline
- // entry
- // with the shortest validity period is always shown if overlapping).
- //
- // We don't need to deal with the case of shortest validity between this entry, and an
- // already
- // chosen candidate time, as if we've got here, the start time of nextEntry is lower
- // than
- // the entry that is driving currentSmallestExpiry, so nextEntry would be shown
- // regardless.
- long nextEntryValidityLength =
- nextEntryValidity.getEndMillis() - nextEntryValidity.getStartMillis();
-
- if (nextEntryValidityLength < entryValidityLength) {
- // It's valid!
- currentSmallestExpiry = nextEntryValidity.getStartMillis();
- }
- }
-
- return currentSmallestExpiry;
- }
-
- private static boolean isTimeIntervalValid(TimeInterval timeInterval) {
- // Zero-width (and "negative width") validity periods are not valid, and should never be
- // considered.
- return timeInterval.getEndMillis() > timeInterval.getStartMillis();
+ return mCache.findCurrentTimelineEntryExpiry(entry.toProto(), fromTimeMillis);
}
}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
index b159cdb..7246d0d 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/TilesTimelineManager.java
@@ -16,20 +16,15 @@
package androidx.wear.tiles.timeline;
-import static java.lang.Math.max;
-
import android.app.AlarmManager;
-import android.app.AlarmManager.OnAlarmListener;
-import android.util.Log;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.wear.tiles.builders.LayoutElementBuilders;
import androidx.wear.tiles.builders.TimelineBuilders;
+import androidx.wear.tiles.timeline.internal.TilesTimelineManagerInternal;
import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
/**
* Manager for a single Wear Tiles timeline.
@@ -38,9 +33,9 @@
* correct alarms to detect when a layout should be updated, and dispatch it to its listener.
*/
public class TilesTimelineManager {
- // 1 minute min delay between tiles.
@VisibleForTesting
- static final long MIN_TILE_UPDATE_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1);
+ static final long MIN_TILE_UPDATE_DELAY_MILLIS =
+ TilesTimelineManagerInternal.MIN_TILE_UPDATE_DELAY_MILLIS;
/** Interface so this manager can retrieve the current time. */
public interface Clock {
@@ -60,15 +55,7 @@
void onLayoutUpdate(int token, @NonNull LayoutElementBuilders.Layout layout);
}
- private static final String TAG = "TimelineManager";
-
- private final AlarmManager mAlarmManager;
- private final Clock mClock;
- private final TilesTimelineCache mCache;
- private final Executor mListenerExecutor;
- private final Listener mListener;
- private final int mToken;
- @Nullable private OnAlarmListener mAlarmListener = null;
+ private final TilesTimelineManagerInternal mManager;
/**
* Default constructor.
@@ -86,13 +73,17 @@
@NonNull TimelineBuilders.Timeline timeline,
int token,
@NonNull Executor listenerExecutor,
- @NonNull Listener listener) {
- this.mAlarmManager = alarmManager;
- this.mClock = clock;
- this.mCache = new TilesTimelineCache(timeline);
- this.mToken = token;
- this.mListenerExecutor = listenerExecutor;
- this.mListener = listener;
+ @NonNull Listener listener
+ ) {
+ mManager = new TilesTimelineManagerInternal(
+ alarmManager,
+ () -> clock.getCurrentTimeMillis(),
+ timeline.toProto(),
+ token,
+ listenerExecutor,
+ (t, entry) -> listener.onLayoutUpdate(t,
+ LayoutElementBuilders.Layout.fromProto(entry.getLayout()))
+ );
}
/**
@@ -100,63 +91,11 @@
* layout, and set its first alarm.
*/
public void init() {
- dispatchNextLayout();
+ mManager.init();
}
/** Tears down this Timeline Manager. This will ensure any set alarms are cleared up. */
public void deInit() {
- if (mAlarmListener != null) {
- mAlarmManager.cancel(mAlarmListener);
- mAlarmListener = null;
- }
- }
-
- void dispatchNextLayout() {
- if (mAlarmListener != null) {
- mAlarmManager.cancel(mAlarmListener);
- mAlarmListener = null;
- }
-
- long now = mClock.getCurrentTimeMillis();
- TimelineBuilders.TimelineEntry entry = mCache.findTimelineEntryForTime(now);
-
- if (entry == null) {
- Log.d(TAG, "Could not find absolute timeline entry for time " + now);
-
- entry = mCache.findClosestTimelineEntry(now);
-
- if (entry == null) {
- Log.w(TAG, "Could not find any timeline entry for time " + now);
- return;
- }
- }
-
- // Find when this entry should expire, and set a rollover alarm.
- long expiryTime = mCache.findCurrentTimelineEntryExpiry(entry, now);
-
- expiryTime = max(expiryTime, now + MIN_TILE_UPDATE_DELAY_MILLIS);
-
- if (expiryTime != Long.MAX_VALUE) {
- // This **has** to be an instantiation like this, in order for AlarmManager#cancel to
- // work
- // correctly (it doesn't work on method references).
- mAlarmListener =
- new OnAlarmListener() {
- @Override
- public void onAlarm() {
- dispatchNextLayout();
- }
- };
-
- // Run on the main thread (targetHandler = null). The update has to be on the main
- // thread so
- // it can mutate the layout, so we might as well just do everything there.
- mAlarmManager.set(
- AlarmManager.RTC, expiryTime, TAG, mAlarmListener, /* targetHandler= */ null);
- }
-
- final LayoutElementBuilders.Layout layout = LayoutElementBuilders.Layout.fromProto(
- entry.toProto().getLayout());
- mListenerExecutor.execute(() -> mListener.onLayoutUpdate(mToken, layout));
+ mManager.deInit();
}
}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java
new file mode 100644
index 0000000..b7e8029
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineCacheInternal.java
@@ -0,0 +1,237 @@
+/*
+ * 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.tiles.timeline.internal;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.wear.tiles.proto.TimelineProto.TimeInterval;
+import androidx.wear.tiles.proto.TimelineProto.Timeline;
+import androidx.wear.tiles.proto.TimelineProto.TimelineEntry;
+import androidx.wear.tiles.timeline.TilesTimelineCache;
+
+/**
+ * Timeline cache for Wear Tiles. This will take in a full timeline, and return the appropriate
+ * entry for the given time from {@code findTimelineEntryForTime}.
+ */
+public final class TilesTimelineCacheInternal {
+ private final Timeline mTimeline;
+
+ public TilesTimelineCacheInternal(@NonNull Timeline timeline) {
+ this.mTimeline = timeline;
+ }
+
+ /**
+ * Finds the entry which should be active at the given time. This will return the entry which
+ * has the _shortest_ validity period at the current time, if validity periods overlap. Note
+ * that an entry which has no validity period set will be considered a "default" and will be
+ * used if no other entries are suitable.
+ *
+ * @param timeMillis The time to base the search on, in milliseconds.
+ * @return The timeline entry which should be active at the given time. Returns {@code null} if
+ * none are valid.
+ */
+ @MainThread
+ @Nullable
+ public TimelineEntry findTimelineEntryForTime(long timeMillis) {
+ TimelineEntry currentEntry = null;
+ long currentEntryLength = Long.MAX_VALUE;
+
+ // Iterate through, finding the _shortest_ valid timeline entry.
+ for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
+ if (!entry.hasValidity()) {
+ // Only override a default if there's no more specific entry found.
+ if (currentEntryLength == Long.MAX_VALUE) {
+ // Let's treat an entry with no validity as being a "default", as long as we
+ // haven't found
+ // any other valid entries
+ currentEntry = entry;
+ }
+ } else {
+ TimeInterval validity = entry.getValidity();
+
+ long validityLength = validity.getEndMillis() - validity.getStartMillis();
+
+ if (validityLength > currentEntryLength) {
+ continue;
+ }
+
+ if (validity.getStartMillis() <= timeMillis
+ && timeMillis < validity.getEndMillis()) {
+ currentEntry = entry;
+ currentEntryLength = validityLength;
+ }
+ }
+ }
+
+ return currentEntry;
+ }
+
+ /**
+ * A (very) inexact version of {@link TilesTimelineCache#findTimelineEntryForTime(long)} which
+ * finds the closest timeline entry to the current time, regardless of validity. This should
+ * only used as a fallback if {@code findTimelineEntryForTime} fails, so it can attempt to at
+ * least show something.
+ *
+ * <p>By this point, we're technically in an error state, so just show _something_. Note that
+ * calling this if {@code findTimelineEntryForTime} returns a valid entry is invalid, and may
+ * lead to incorrect results.
+ *
+ * @param timeMillis The time to search from, in milliseconds.
+ * @return The timeline entry with validity period closest to {@code timeMillis}.
+ */
+ @MainThread
+ @Nullable
+ public TimelineEntry findClosestTimelineEntry(long timeMillis) {
+ long currentEntryError = Long.MAX_VALUE;
+ TimelineEntry currentEntry = null;
+
+ for (TimelineEntry entry : mTimeline.getTimelineEntriesList()) {
+ if (!entry.hasValidity()) {
+ // It's a default. This shouldn't happen if we've been called. Skip it.
+ continue;
+ }
+
+ TimeInterval validity = entry.getValidity();
+
+ if (!isTimeIntervalValid(validity)) {
+ continue;
+ }
+
+ // It's valid in this time interval. Shouldn't happen. Skip anyway.
+ if (validity.getStartMillis() <= timeMillis && timeMillis < validity.getEndMillis()) {
+ continue;
+ }
+
+ long error;
+
+ // It's in the future.
+ if (validity.getStartMillis() > timeMillis) {
+ error = validity.getStartMillis() - timeMillis;
+ } else {
+ // It's in the past.
+ error = timeMillis - validity.getEndMillis();
+ }
+
+ if (error < currentEntryError) {
+ currentEntry = entry;
+ currentEntryError = error;
+ }
+ }
+
+ return currentEntry;
+ }
+
+ /**
+ * Finds when the timeline entry {@code entry} should be considered "expired". This is either
+ * when it is no longer valid (i.e. end_millis), or when another entry should be presented
+ * instead.
+ *
+ * @param entry The entry to find the expiry time of.
+ * @param fromTimeMillis The time to start searching from. The returned time will never be lower
+ * than the value passed here.
+ * @return The time in millis that {@code entry} should be considered to be expired. This value
+ * will be {@link Long#MAX_VALUE} if {@code entry} does not expire.
+ */
+ public long findCurrentTimelineEntryExpiry(
+ @NonNull TimelineEntry entry, long fromTimeMillis) {
+ long currentSmallestExpiry = Long.MAX_VALUE;
+ long entryValidityLength = Long.MAX_VALUE;
+
+ if (entry.hasValidity() && entry.getValidity().getEndMillis() > fromTimeMillis) {
+ currentSmallestExpiry = entry.getValidity().getEndMillis();
+ entryValidityLength =
+ entry.getValidity().getEndMillis()
+ - entry.getValidity().getStartMillis();
+ }
+
+ // Search for the starting edge of an overlapping period (i.e. one with startTime between
+ // entry.startTime and entry.endTime), with a validity period shorter than the one currently
+ // being considered.
+ for (TimelineEntry nextEntry : mTimeline.getTimelineEntriesList()) {
+ // The entry can't invalidate itself
+ if (nextEntry.equals(entry)) {
+ continue;
+ }
+
+ // Discard if nextEntry doesn't have a validity period. In this case, it's a default (so
+ // would potentially be used at entry.end_millis anyway).
+ if (!nextEntry.hasValidity()) {
+ continue;
+ }
+
+ TimeInterval nextEntryValidity = nextEntry.getValidity();
+
+ // Discard if the validity period is flat out invalid.
+ if (!isTimeIntervalValid(nextEntryValidity)) {
+ continue;
+ }
+
+ // Discard if the start time of nextEntry doesn't fall in the current period (it can't
+ // interrupt this entry, so this entry's expiry should be used).
+ if (entry.hasValidity()) {
+ if (nextEntryValidity.getStartMillis() > entry.getValidity().getEndMillis()
+ || nextEntryValidity.getStartMillis()
+ < entry.getValidity().getStartMillis()) {
+ continue;
+ }
+ }
+
+ // Discard if its start time is greater than the current smallest one we've found. In
+ // that
+ // case, the entry that gave us currentSmallestExpiry would be shown next.
+ if (nextEntryValidity.getStartMillis() > currentSmallestExpiry) {
+ continue;
+ }
+
+ // Discard if it's less than "fromTime". This prevents accidentally returning valid
+ // times in
+ // the past.
+ if (nextEntryValidity.getStartMillis() < fromTimeMillis) {
+ continue;
+ }
+
+ // Finally, consider whether the length of the validity period is shorter than the
+ // current
+ // one. If this doesn't hold, the current entry would be shown instead (the timeline
+ // entry
+ // with the shortest validity period is always shown if overlapping).
+ //
+ // We don't need to deal with the case of shortest validity between this entry, and an
+ // already
+ // chosen candidate time, as if we've got here, the start time of nextEntry is lower
+ // than
+ // the entry that is driving currentSmallestExpiry, so nextEntry would be shown
+ // regardless.
+ long nextEntryValidityLength =
+ nextEntryValidity.getEndMillis() - nextEntryValidity.getStartMillis();
+
+ if (nextEntryValidityLength < entryValidityLength) {
+ // It's valid!
+ currentSmallestExpiry = nextEntryValidity.getStartMillis();
+ }
+ }
+
+ return currentSmallestExpiry;
+ }
+
+ private static boolean isTimeIntervalValid(TimeInterval timeInterval) {
+ // Zero-width (and "negative width") validity periods are not valid, and should never be
+ // considered.
+ return timeInterval.getEndMillis() > timeInterval.getStartMillis();
+ }
+}
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java
new file mode 100644
index 0000000..abf1505
--- /dev/null
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/TilesTimelineManagerInternal.java
@@ -0,0 +1,160 @@
+/*
+ * 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.tiles.timeline.internal;
+
+import static java.lang.Math.max;
+
+import android.app.AlarmManager;
+import android.app.AlarmManager.OnAlarmListener;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.wear.tiles.proto.TimelineProto;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Manager for a single Wear Tiles timeline.
+ *
+ * <p>This handles the dispatching of single Tile layouts from a full timeline. It will set the
+ * correct alarms to detect when a layout should be updated, and dispatch it to its listener.
+ */
+public class TilesTimelineManagerInternal {
+ // 1 minute min delay between tiles.
+ @VisibleForTesting
+ public static final long MIN_TILE_UPDATE_DELAY_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
+ /** Interface so this manager can retrieve the current time. */
+ public interface Clock {
+ /** Get the current wall-clock time in millis. */
+ long getCurrentTimeMillis();
+ }
+
+ /** Type to listen for layout updates from a given timeline. */
+ public interface Listener {
+
+ /**
+ * Called when a timeline has a new layout to be displayed.
+ *
+ * @param token The token originally passed to {@link TilesTimelineManagerInternal}.
+ * @param entry The new timeline entry to use.
+ */
+ void onLayoutUpdate(int token, @NonNull TimelineProto.TimelineEntry entry);
+ }
+
+ private static final String TAG = "TimelineManager";
+
+ private final AlarmManager mAlarmManager;
+ private final Clock mClock;
+ private final TilesTimelineCacheInternal mCache;
+ private final Executor mListenerExecutor;
+ private final Listener mListener;
+ private final int mToken;
+ @Nullable private OnAlarmListener mAlarmListener = null;
+
+ /**
+ * Default constructor.
+ *
+ * @param alarmManager An AlarmManager instance suitable for setting RTC alarms on.
+ * @param clock A Clock to use to ascertain the current time (and hence which tile to show).
+ * This should be synchronized to the same clock as used by {@code alarmManager}
+ * @param timeline The Tiles timeline to use.
+ * @param token A token, which will be passed to {@code listener}'s callback.
+ * @param listener A listener instance, called when a new timeline entry is available.
+ */
+ public TilesTimelineManagerInternal(
+ @NonNull AlarmManager alarmManager,
+ @NonNull Clock clock,
+ @NonNull TimelineProto.Timeline timeline,
+ int token,
+ @NonNull Executor listenerExecutor,
+ @NonNull Listener listener) {
+ this.mAlarmManager = alarmManager;
+ this.mClock = clock;
+ this.mCache = new TilesTimelineCacheInternal(timeline);
+ this.mToken = token;
+ this.mListenerExecutor = listenerExecutor;
+ this.mListener = listener;
+ }
+
+ /**
+ * Sets up this Timeline Manager. This will cause the timeline manager to dispatch the first
+ * layout, and set its first alarm.
+ */
+ public void init() {
+ dispatchNextLayout();
+ }
+
+ /** Tears down this Timeline Manager. This will ensure any set alarms are cleared up. */
+ public void deInit() {
+ if (mAlarmListener != null) {
+ mAlarmManager.cancel(mAlarmListener);
+ mAlarmListener = null;
+ }
+ }
+
+ void dispatchNextLayout() {
+ if (mAlarmListener != null) {
+ mAlarmManager.cancel(mAlarmListener);
+ mAlarmListener = null;
+ }
+
+ long now = mClock.getCurrentTimeMillis();
+ TimelineProto.TimelineEntry entry = mCache.findTimelineEntryForTime(now);
+
+ if (entry == null) {
+ Log.d(TAG, "Could not find absolute timeline entry for time " + now);
+
+ entry = mCache.findClosestTimelineEntry(now);
+
+ if (entry == null) {
+ Log.w(TAG, "Could not find any timeline entry for time " + now);
+ return;
+ }
+ }
+
+ // Find when this entry should expire, and set a rollover alarm.
+ long expiryTime = mCache.findCurrentTimelineEntryExpiry(entry, now);
+
+ expiryTime = max(expiryTime, now + MIN_TILE_UPDATE_DELAY_MILLIS);
+
+ if (expiryTime != Long.MAX_VALUE) {
+ // This **has** to be an instantiation like this, in order for AlarmManager#cancel to
+ // work
+ // correctly (it doesn't work on method references).
+ mAlarmListener =
+ new OnAlarmListener() {
+ @Override
+ public void onAlarm() {
+ dispatchNextLayout();
+ }
+ };
+
+ // Run on the main thread (targetHandler = null). The update has to be on the main
+ // thread so
+ // it can mutate the layout, so we might as well just do everything there.
+ mAlarmManager.set(
+ AlarmManager.RTC, expiryTime, TAG, mAlarmListener, /* targetHandler= */ null);
+ }
+
+ final TimelineProto.TimelineEntry entryToDispatch = entry;
+ mListenerExecutor.execute(() -> mListener.onLayoutUpdate(mToken, entryToDispatch));
+ }
+}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/package-info.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
similarity index 77%
rename from work/workmanager/src/main/java/androidx/work/impl/package-info.java
rename to wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
index 3f9ab0d..92063a4 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/package-info.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/timeline/internal/package-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 The Android Open Source Project
+ * 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.
@@ -15,9 +15,11 @@
*/
/**
+ * Internal implementation for Wear Tiles.
+ *
* @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-package androidx.work.impl;
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+package androidx.wear.tiles.timeline.internal;
import androidx.annotation.RestrictTo;
diff --git a/wear/wear-complications-data/api/public_plus_experimental_current.txt b/wear/wear-complications-data/api/public_plus_experimental_current.txt
index 40ba553..8829abf 100644
--- a/wear/wear-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/wear-complications-data/api/public_plus_experimental_current.txt
@@ -243,7 +243,7 @@
}
public final class DataKt {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData toApiComplicationData(android.support.wearable.complications.ComplicationData);
}
public final class EmptyComplicationData extends androidx.wear.complications.data.ComplicationData {
diff --git a/wear/wear-complications-data/api/restricted_current.txt b/wear/wear-complications-data/api/restricted_current.txt
index a315bbc..2c16c7d 100644
--- a/wear/wear-complications-data/api/restricted_current.txt
+++ b/wear/wear-complications-data/api/restricted_current.txt
@@ -259,11 +259,11 @@
}
public interface ComplicationText {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.support.wearable.complications.ComplicationText asWireComplicationText();
method public long getNextChangeTime(long fromDateTimeMillis);
method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
method public boolean isAlwaysEmpty();
method public boolean returnsSameText(long firstDateTimeMillis, long secondDateTimeMillis);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.support.wearable.complications.ComplicationText toWireComplicationText();
}
public enum ComplicationType {
@@ -301,7 +301,7 @@
}
public final class DataKt {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData asApiComplicationData(android.support.wearable.complications.ComplicationData);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationData toApiComplicationData(android.support.wearable.complications.ComplicationData);
}
public final class EmptyComplicationData extends androidx.wear.complications.data.ComplicationData {
@@ -535,6 +535,7 @@
}
public final class TextKt {
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationText toApiComplicationText(android.support.wearable.complications.TimeDependentText);
}
public final class TimeDifferenceComplicationText implements androidx.wear.complications.data.ComplicationText {
@@ -594,8 +595,8 @@
}
public final class TypeKt {
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationType![] asApiComplicationTypes(int[]);
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static int[] asWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType>);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static androidx.wear.complications.data.ComplicationType![] toApiComplicationTypes(int[]);
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public static int[] toWireTypes(java.util.Collection<? extends androidx.wear.complications.data.ComplicationType>);
}
}
diff --git a/wear/wear-complications-data/build.gradle b/wear/wear-complications-data/build.gradle
index dc18da2..d004c40 100644
--- a/wear/wear-complications-data/build.gradle
+++ b/wear/wear-complications-data/build.gradle
@@ -64,7 +64,7 @@
name = "Android Wear Complications"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_COMPLICATIONS
+ mavenVersion = LibraryVersions.WEAR_COMPLICATIONS_DATA
inceptionYear = "2020"
description = "Android Wear Complications"
}
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
index c74a757..a0a7b71 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ComplicationHelperActivity.java
@@ -231,7 +231,7 @@
int[] wireSupportedTypes = new int[supportedTypes.size()];
int i = 0;
for (ComplicationType supportedType : supportedTypes) {
- wireSupportedTypes[i++] = supportedType.asWireComplicationType();
+ wireSupportedTypes[i++] = supportedType.toWireComplicationType();
}
intent.putExtra(ProviderChooserIntent.EXTRA_SUPPORTED_TYPES, wireSupportedTypes);
return intent;
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
index 0ab5a45..70dea0a 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/ProviderInfoRetriever.kt
@@ -30,7 +30,7 @@
import androidx.annotation.VisibleForTesting
import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationType
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.utility.TraceEvent
import kotlinx.coroutines.CompletableDeferred
@@ -152,13 +152,13 @@
service.asBinder().linkToDeath(deathObserver, 0)
if (!service.requestPreviewComplicationData(
providerComponent,
- complicationType.asWireComplicationType(),
+ complicationType.toWireComplicationType(),
object : IPreviewComplicationDataCallback.Stub() {
override fun updateComplicationData(
data: android.support.wearable.complications.ComplicationData?
) {
service.asBinder().unlinkToDeath(deathObserver, 0)
- result.complete(data?.asApiComplicationData())
+ result.complete(data?.toApiComplicationData())
}
}
)
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
index b701dbe..14a7de5 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Data.kt
@@ -54,7 +54,7 @@
internal fun createWireComplicationDataBuilder(): WireComplicationDataBuilder =
cachedWireComplicationData?.let {
WireComplicationDataBuilder(it)
- } ?: WireComplicationDataBuilder(type.asWireComplicationType())
+ } ?: WireComplicationDataBuilder(type.toWireComplicationType())
}
/**
@@ -200,9 +200,9 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
- setShortText(text.asWireComplicationText())
- setShortTitle(title?.asWireComplicationText())
- setContentDescription(contentDescription?.asWireComplicationText())
+ setShortText(text.toWireComplicationText())
+ setShortTitle(title?.toWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
@@ -308,12 +308,12 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
- setLongText(text.asWireComplicationText())
- setLongTitle(title?.asWireComplicationText())
+ setLongText(text.toWireComplicationText())
+ setLongTitle(title?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
smallImage?.addToWireComplicationData(this)
setTapAction(tapAction)
- setContentDescription(contentDescription?.asWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
@@ -429,10 +429,10 @@
setRangedMinValue(min)
setRangedMaxValue(max)
monochromaticImage?.addToWireComplicationData(this)
- setShortText(text?.asWireComplicationText())
- setShortTitle(title?.asWireComplicationText())
+ setShortText(text?.toWireComplicationText())
+ setShortTitle(title?.toWireComplicationText())
setTapAction(tapAction)
- setContentDescription(contentDescription?.asWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
@@ -512,7 +512,7 @@
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
monochromaticImage.addToWireComplicationData(this)
- setContentDescription(contentDescription?.asWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
@@ -593,7 +593,7 @@
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
smallImage.addToWireComplicationData(this)
- setContentDescription(contentDescription?.asWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
setTapAction(tapAction)
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
@@ -680,7 +680,7 @@
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
setLargeImage(photoImage)
- setContentDescription(contentDescription?.asWireComplicationText())
+ setContentDescription(contentDescription?.toWireComplicationText())
setValidTimeRange(validTimeRange, this)
}.build().also { cachedWireComplicationData = it }
@@ -757,8 +757,8 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
override fun asWireComplicationData(): WireComplicationData =
createWireComplicationDataBuilder().apply {
- setShortText(text?.asWireComplicationText())
- setShortTitle(title?.asWireComplicationText())
+ setShortText(text?.toWireComplicationText())
+ setShortTitle(title?.toWireComplicationText())
monochromaticImage?.addToWireComplicationData(this)
}.build().also { cachedWireComplicationData = it }
@@ -771,38 +771,38 @@
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun WireComplicationData.asApiComplicationData(): ComplicationData {
+public fun WireComplicationData.toApiComplicationData(): ComplicationData {
val wireComplicationData = this
return when (type) {
- NoDataComplicationData.TYPE.asWireComplicationType() -> NoDataComplicationData()
+ NoDataComplicationData.TYPE.toWireComplicationType() -> NoDataComplicationData()
- EmptyComplicationData.TYPE.asWireComplicationType() -> EmptyComplicationData()
+ EmptyComplicationData.TYPE.toWireComplicationType() -> EmptyComplicationData()
- NotConfiguredComplicationData.TYPE.asWireComplicationType() ->
+ NotConfiguredComplicationData.TYPE.toWireComplicationType() ->
NotConfiguredComplicationData()
- ShortTextComplicationData.TYPE.asWireComplicationType() ->
- ShortTextComplicationData.Builder(shortText!!.asApiComplicationText()).apply {
+ ShortTextComplicationData.TYPE.toWireComplicationType() ->
+ ShortTextComplicationData.Builder(shortText!!.toApiComplicationText()).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setTitle(shortTitle?.asApiComplicationText())
+ setTitle(shortTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- LongTextComplicationData.TYPE.asWireComplicationType() ->
- LongTextComplicationData.Builder(longText!!.asApiComplicationText()).apply {
+ LongTextComplicationData.TYPE.toWireComplicationType() ->
+ LongTextComplicationData.Builder(longText!!.toApiComplicationText()).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setTitle(longTitle?.asApiComplicationText())
+ setTitle(longTitle?.toApiComplicationText())
setMonochromaticImage(parseIcon())
setSmallImage(parseSmallImage())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- RangedValueComplicationData.TYPE.asWireComplicationType() ->
+ RangedValueComplicationData.TYPE.toWireComplicationType() ->
RangedValueComplicationData.Builder(
value = rangedValue, min = rangedMinValue,
max = rangedMaxValue
@@ -810,40 +810,40 @@
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
setMonochromaticImage(parseIcon())
- setTitle(shortTitle?.asApiComplicationText())
- setText(shortText?.asApiComplicationText())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setTitle(shortTitle?.toApiComplicationText())
+ setText(shortText?.toApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- MonochromaticImageComplicationData.TYPE.asWireComplicationType() ->
+ MonochromaticImageComplicationData.TYPE.toWireComplicationType() ->
MonochromaticImageComplicationData.Builder(parseIcon()!!).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- SmallImageComplicationData.TYPE.asWireComplicationType() ->
+ SmallImageComplicationData.TYPE.toWireComplicationType() ->
SmallImageComplicationData.Builder(parseSmallImage()!!).apply {
setTapAction(tapAction)
setValidTimeRange(parseTimeRange())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- PhotoImageComplicationData.TYPE.asWireComplicationType() ->
+ PhotoImageComplicationData.TYPE.toWireComplicationType() ->
PhotoImageComplicationData.Builder(largeImage!!).apply {
setValidTimeRange(parseTimeRange())
- setContentDescription(contentDescription?.asApiComplicationText())
+ setContentDescription(contentDescription?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
- NoPermissionComplicationData.TYPE.asWireComplicationType() ->
+ NoPermissionComplicationData.TYPE.toWireComplicationType() ->
NoPermissionComplicationData.Builder().apply {
setMonochromaticImage(parseIcon())
- setTitle(shortTitle?.asApiComplicationText())
- setText(shortText?.asApiComplicationText())
+ setTitle(shortTitle?.toApiComplicationText())
+ setText(shortText?.toApiComplicationText())
setCachedWireComplicationData(wireComplicationData)
}.build()
@@ -879,7 +879,7 @@
/** Some of the types, do not have any fields. This method provides a shorthard for that case. */
internal fun asPlainWireComplicationData(type: ComplicationType) =
- WireComplicationDataBuilder(type.asWireComplicationType()).build()
+ WireComplicationDataBuilder(type.toWireComplicationType()).build()
internal fun setValidTimeRange(validTimeRange: TimeRange?, data: WireComplicationDataBuilder) {
validTimeRange?.let {
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
index c05db28..fd88d45 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Text.kt
@@ -30,6 +30,8 @@
private typealias WireComplicationTextTimeFormatBuilder =
android.support.wearable.complications.ComplicationText.TimeFormatBuilder
+private typealias WireTimeDependentText = android.support.wearable.complications.TimeDependentText
+
/**
* The text within a complication.
*
@@ -69,7 +71,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public fun asWireComplicationText(): WireComplicationText
+ public fun toWireComplicationText(): WireComplicationText
}
/** A [ComplicationText] that contains plain text. */
@@ -351,13 +353,42 @@
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationText() = delegate
+ override fun toWireComplicationText() = delegate
}
/** Converts a [WireComplicationText] into an equivalent [ComplicationText] instead. */
-internal fun WireComplicationText.asApiComplicationText(): ComplicationText =
+internal fun WireComplicationText.toApiComplicationText(): ComplicationText =
DelegatingComplicationText(this)
/** Converts a [TimeZone] into an equivalent [java.util.TimeZone]. */
internal fun TimeZone.asJavaTimeZone(): java.util.TimeZone =
java.util.TimeZone.getTimeZone(this.id)
+
+/** [ComplicationText] implementation that delegates to a [WireTimeDependentText] instance. */
+private class DelegatingTimeDependentText(
+ private val delegate: WireTimeDependentText
+) : ComplicationText {
+ override fun getTextAt(resources: Resources, dateTimeMillis: Long) =
+ delegate.getTextAt(resources, dateTimeMillis)
+
+ override fun returnsSameText(firstDateTimeMillis: Long, secondDateTimeMillis: Long) =
+ delegate.returnsSameText(firstDateTimeMillis, secondDateTimeMillis)
+
+ override fun getNextChangeTime(fromDateTimeMillis: Long) =
+ delegate.getNextChangeTime(fromDateTimeMillis)
+
+ override fun isAlwaysEmpty() = false
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ override fun toWireComplicationText(): WireComplicationText {
+ throw UnsupportedOperationException(
+ "DelegatingTimeDependentText doesn't support asWireComplicationText"
+ )
+ }
+}
+
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun WireTimeDependentText.toApiComplicationText(): ComplicationText =
+ DelegatingTimeDependentText(this)
diff --git a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
index 4c9eb33..97d4dfc 100644
--- a/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
+++ b/wear/wear-complications-data/src/main/java/androidx/wear/complications/data/Type.kt
@@ -39,7 +39,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- public fun asWireComplicationType(): Int = wireType
+ public fun toWireComplicationType(): Int = wireType
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -80,7 +80,7 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
- public fun toWireTypes(types: Collection<ComplicationType>): IntArray = types.asWireTypes()
+ public fun toWireTypes(types: Collection<ComplicationType>): IntArray = types.toWireTypes()
/**
* Converts an array of integer values used for serialization into the corresponding array
@@ -95,7 +95,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
public fun fromWireTypes(types: IntArray): Array<ComplicationType> =
- types.asApiComplicationTypes()
+ types.toApiComplicationTypes()
/**
* Converts an array of integer values used for serialization into the corresponding list
@@ -119,8 +119,8 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun Collection<ComplicationType>.asWireTypes(): IntArray =
- this.map { it.asWireComplicationType() }.toIntArray()
+public fun Collection<ComplicationType>.toWireTypes(): IntArray =
+ this.map { it.toWireComplicationType() }.toIntArray()
/**
* Converts an array of integer values uses for serialization into the corresponding array
@@ -131,5 +131,5 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun IntArray.asApiComplicationTypes(): Array<ComplicationType> =
+public fun IntArray.toApiComplicationTypes(): Array<ComplicationType> =
this.map { ComplicationType.fromWireType(it) }.toTypedArray()
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
index d0da8608..6c7fac6 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/ProviderInfoRetrieverTest.kt
@@ -75,7 +75,7 @@
true
}.`when`(mockService).requestPreviewComplicationData(
eq(component),
- eq(type.asWireComplicationType()),
+ eq(type.toWireComplicationType()),
any()
)
@@ -104,7 +104,7 @@
true
}.`when`(mockService).requestPreviewComplicationData(
eq(component),
- eq(type.asWireComplicationType()),
+ eq(type.toWireComplicationType()),
any()
)
@@ -137,7 +137,7 @@
false
}.`when`(mockService).requestPreviewComplicationData(
eq(component),
- eq(type.asWireComplicationType()),
+ eq(type.toWireComplicationType()),
any()
)
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
index 9d93102..7495485 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/DataTest.kt
@@ -177,7 +177,7 @@
private fun testRoundTripConversions(data: ComplicationData) {
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- data.asWireComplicationData().asApiComplicationData().asWireComplicationData()
+ data.asWireComplicationData().toApiComplicationData().asWireComplicationData()
)
}
}
@@ -294,7 +294,7 @@
}
private fun assertRoundtrip(wireData: WireComplicationData, type: ComplicationType) {
- val data = wireData.asApiComplicationData()
+ val data = wireData.toApiComplicationData()
assertThat(data.type).isEqualTo(type)
ParcelableSubject.assertThat(data.asWireComplicationData()).hasSameSerializationAs(wireData)
}
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
index aa1c765..7d3843c 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TextTest.kt
@@ -38,9 +38,9 @@
@Test
public fun plainText() {
val text = PlainComplicationText.Builder("abc").build()
- ParcelableSubject.assertThat(text.asWireComplicationText())
+ ParcelableSubject.assertThat(text.toWireComplicationText())
.hasSameSerializationAs(WireComplicationText.plainText("abc"))
- ParcelableSubject.assertThat(text.asWireComplicationText())
+ ParcelableSubject.assertThat(text.toWireComplicationText())
.hasDifferentSerializationAs(WireComplicationText.plainText("abc1"))
}
@@ -56,7 +56,7 @@
.setMinimumUnit(TimeUnit.SECONDS)
.build()
- ParcelableSubject.assertThat(text.asWireComplicationText())
+ ParcelableSubject.assertThat(text.toWireComplicationText())
.hasSameSerializationAs(
WireTimeDifferenceBuilder()
.setStyle(WireComplicationText.DIFFERENCE_STYLE_STOPWATCH)
@@ -85,7 +85,7 @@
.setMinimumUnit(TimeUnit.SECONDS)
.build()
- ParcelableSubject.assertThat(text.asWireComplicationText())
+ ParcelableSubject.assertThat(text.toWireComplicationText())
.hasSameSerializationAs(
WireTimeDifferenceBuilder()
.setStyle(WireComplicationText.DIFFERENCE_STYLE_STOPWATCH)
@@ -110,7 +110,7 @@
.setTimeZone(TimeZone.getTimeZone("Europe/London"))
.build()
- ParcelableSubject.assertThat(text.asWireComplicationText())
+ ParcelableSubject.assertThat(text.toWireComplicationText())
.hasSameSerializationAs(
WireTimeFormatBuilder()
.setFormat("h:m")
@@ -129,7 +129,7 @@
@Test
public fun plainText() {
val wireText = WireComplicationText.plainText("abc")
- val text = wireText.asApiComplicationText()
+ val text = wireText.toApiComplicationText()
assertThat(text.getTextAt(getResource(), 0)).isEqualTo("abc")
assertThat(text.getNextChangeTime(0)).isEqualTo(Long.MAX_VALUE)
@@ -148,7 +148,7 @@
.setReferencePeriodEndMillis(startPointMillis)
.build()
- val text = wireText.asApiComplicationText()
+ val text = wireText.toApiComplicationText()
val twoMinutesThreeSecondAfter = startPointMillis + 2.minutes + 3.seconds
assertThat(
@@ -173,7 +173,7 @@
.setTimeZone(java.util.TimeZone.getTimeZone("Europe/London"))
.build()
- val text = wireText.asApiComplicationText()
+ val text = wireText.toApiComplicationText()
assertThat(text.getTextAt(getResource(), dateTimeMillis).toString())
.isEqualTo("10:15 in London")
diff --git a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
index 76e215b..75c5f30 100644
--- a/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
+++ b/wear/wear-complications-data/src/test/java/androidx/wear/complications/data/TypeTest.kt
@@ -47,7 +47,7 @@
}
private fun assertThatIsWireType(type: ComplicationType, wireType: Int) {
- assertThat(type.asWireComplicationType()).isEqualTo(wireType)
+ assertThat(type.toWireComplicationType()).isEqualTo(wireType)
}
@Test
diff --git a/wear/wear-complications-provider/build.gradle b/wear/wear-complications-provider/build.gradle
index b36a93a..60d0420 100644
--- a/wear/wear-complications-provider/build.gradle
+++ b/wear/wear-complications-provider/build.gradle
@@ -57,7 +57,7 @@
name = "Android Wear Complications"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_COMPLICATIONS
+ mavenVersion = LibraryVersions.WEAR_COMPLICATIONS_PROVIDER
inceptionYear = "2020"
description = "Android Wear Complications"
}
diff --git a/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java b/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
index c7a459f..3ddb44d 100644
--- a/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
+++ b/wear/wear-complications-provider/src/test/java/androidx/wear/complications/ComplicationProviderServiceTest.java
@@ -135,7 +135,7 @@
public void testOnComplicationUpdate() throws Exception {
int id = 123;
mComplicationProvider.onUpdate(
- id, ComplicationType.LONG_TEXT.asWireComplicationType(), mLocalManager);
+ id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
ShadowLooper.runUiThreadTasks();
ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
@@ -151,7 +151,7 @@
public void testOnComplicationUpdateNoUpdateRequired() throws Exception {
int id = 123;
mNoUpdateComplicationProvider.onUpdate(
- id, ComplicationType.LONG_TEXT.asWireComplicationType(), mLocalManager);
+ id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
ShadowLooper.runUiThreadTasks();
ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
@@ -164,7 +164,7 @@
@Test
public void testGetComplicationPreviewData() throws Exception {
assertThat(mComplicationProvider.getComplicationPreviewData(
- ComplicationType.LONG_TEXT.asWireComplicationType()
+ ComplicationType.LONG_TEXT.toWireComplicationType()
).getLongText().getTextAt(null, 0)).isEqualTo("hello preview");
}
@@ -172,7 +172,7 @@
public void testGetComplicationPreviewDataReturnsNull() throws Exception {
// The ComplicationProvider doesn't support PHOTO_IMAGE so null should be returned.
assertNull(mComplicationProvider.getComplicationPreviewData(
- ComplicationType.PHOTO_IMAGE.asWireComplicationType())
+ ComplicationType.PHOTO_IMAGE.toWireComplicationType())
);
}
}
diff --git a/wear/wear-input-testing/build.gradle b/wear/wear-input-testing/build.gradle
index 28d856d..bcd9ccc 100644
--- a/wear/wear-input-testing/build.gradle
+++ b/wear/wear-input-testing/build.gradle
@@ -38,7 +38,7 @@
name = "Android Wear Support Input Testing Helpers"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_INPUT
+ mavenVersion = LibraryVersions.WEAR_INPUT_TESTING
inceptionYear = "2020"
description = "Android Wear Support Input Testing Helpers"
}
diff --git a/wear/wear-phone-interactions/api/current.txt b/wear/wear-phone-interactions/api/current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/current.txt
+++ b/wear/wear-phone-interactions/api/current.txt
@@ -66,9 +66,9 @@
method protected void finalize();
method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
- field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
- field public static final int ERROR_UNSUPPORTED = 1; // 0x1
- field public static final int NO_ERROR = 0; // 0x0
+ field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+ field public static final int NO_ERROR = -1; // 0xffffffff
}
public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-phone-interactions/api/public_plus_experimental_current.txt
@@ -66,9 +66,9 @@
method protected void finalize();
method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
- field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
- field public static final int ERROR_UNSUPPORTED = 1; // 0x1
- field public static final int NO_ERROR = 0; // 0x0
+ field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+ field public static final int NO_ERROR = -1; // 0xffffffff
}
public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/api/restricted_current.txt b/wear/wear-phone-interactions/api/restricted_current.txt
index 0863cad..310f3a1 100644
--- a/wear/wear-phone-interactions/api/restricted_current.txt
+++ b/wear/wear-phone-interactions/api/restricted_current.txt
@@ -66,9 +66,9 @@
method protected void finalize();
method @UiThread public void sendAuthorizationRequest(androidx.wear.phone.interactions.authentication.OAuthRequest request, androidx.wear.phone.interactions.authentication.RemoteAuthClient.Callback clientCallback);
field public static final androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion Companion;
- field public static final int ERROR_PHONE_UNAVAILABLE = 2; // 0x2
- field public static final int ERROR_UNSUPPORTED = 1; // 0x1
- field public static final int NO_ERROR = 0; // 0x0
+ field public static final int ERROR_PHONE_UNAVAILABLE = 1; // 0x1
+ field public static final int ERROR_UNSUPPORTED = 0; // 0x0
+ field public static final int NO_ERROR = -1; // 0xffffffff
}
public abstract static class RemoteAuthClient.Callback {
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
index e1c5d72..c547fc7 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthClient.kt
@@ -134,13 +134,13 @@
"android.support.wearable.authentication.action.OAUTH"
/** Indicates 3p authentication is finished without error */
- public const val NO_ERROR: Int = 0
+ public const val NO_ERROR: Int = -1
/** Indicates 3p authentication isn't supported by Wear OS */
- public const val ERROR_UNSUPPORTED: Int = 1
+ public const val ERROR_UNSUPPORTED: Int = 0
/** Indicates no phone is connected, or the phone connected doesn't support 3p auth */
- public const val ERROR_PHONE_UNAVAILABLE: Int = 2
+ public const val ERROR_PHONE_UNAVAILABLE: Int = 1
/** Errors returned in [.Callback.onAuthorizationError]. */
@Retention(AnnotationRetention.SOURCE)
@@ -339,7 +339,7 @@
}
@SuppressLint("SyntheticAccessor")
- internal fun onResult(response: OAuthResponse) {
+ private fun onResult(response: OAuthResponse) {
@ErrorCode val error = response.getErrorCode()
uiThreadExecutor.execute(
Runnable {
diff --git a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
index 747bb43..dadd75b 100644
--- a/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
+++ b/wear/wear-phone-interactions/src/main/java/androidx/wear/phone/interactions/authentication/RemoteAuthService.kt
@@ -27,6 +27,9 @@
import android.os.RemoteException
import android.support.wearable.authentication.IAuthenticationRequestCallback
import android.support.wearable.authentication.IAuthenticationRequestService
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_ERROR_CODE
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_PACKAGE_NAME
+import androidx.wear.phone.interactions.authentication.RemoteAuthClient.Companion.KEY_RESPONSE_URL
import java.security.SecureRandom
/**
@@ -79,7 +82,7 @@
public companion object {
@JvmStatic
private val callbacksByPackageNameAndRequestID:
- MutableMap<Pair<String, Int>, RemoteAuthClient.RequestCallback> = HashMap()
+ MutableMap<Pair<String, Int>, IAuthenticationRequestCallback> = HashMap()
/**
* To be called by the child class to invoke the callback with Response
@@ -91,7 +94,9 @@
packageNameAndRequestId: Pair<String, Int>
) {
try {
- callbacksByPackageNameAndRequestID[packageNameAndRequestId]?.onResult(response)
+ callbacksByPackageNameAndRequestID[packageNameAndRequestId]?.onResult(
+ buildBundleFromResponse(response, packageNameAndRequestId.first)
+ )
callbacksByPackageNameAndRequestID.remove(packageNameAndRequestId)
} catch (e: RemoteException) {
throw e.cause!!
@@ -99,8 +104,15 @@
}
internal fun getCallback(packageNameAndRequestId: Pair<String, Int>):
- RemoteAuthClient.RequestCallback? =
+ IAuthenticationRequestCallback? =
callbacksByPackageNameAndRequestID[packageNameAndRequestId]
+
+ internal fun buildBundleFromResponse(response: OAuthResponse, packageName: String): Bundle =
+ Bundle().apply {
+ putParcelable(KEY_RESPONSE_URL, response.getResponseUrl())
+ putInt(KEY_ERROR_CODE, response.getErrorCode())
+ putString(KEY_PACKAGE_NAME, packageName)
+ }
}
private val secureRandom: SecureRandom = SecureRandom()
@@ -151,15 +163,15 @@
request: Bundle,
authenticationRequestCallback: IAuthenticationRequestCallback
) {
+ val packageName = request.getString(RemoteAuthClient.KEY_PACKAGE_NAME)
if (remoteAuthRequestHandler.isAuthSupported()) {
- val packageName = request.getString(RemoteAuthClient.KEY_PACKAGE_NAME)
if (!verifyPackageName(context, packageName)) {
throw SecurityException("Failed to verify the Requester's package name")
}
val packageNameAndRequestId = Pair(packageName!!, secureRandom.nextInt())
callbacksByPackageNameAndRequestID[packageNameAndRequestId] =
- authenticationRequestCallback as RemoteAuthClient.RequestCallback
+ authenticationRequestCallback
val requestUrl: Uri? = request.getParcelable(RemoteAuthClient.KEY_REQUEST_URL)
remoteAuthRequestHandler.sendAuthRequest(
@@ -167,9 +179,8 @@
packageNameAndRequestId
)
} else {
- (authenticationRequestCallback as RemoteAuthClient.RequestCallback).onResult(
- OAuthResponse.Builder()
- .setErrorCode(RemoteAuthClient.ERROR_UNSUPPORTED).build()
+ authenticationRequestCallback.onResult(
+ Bundle().apply { putInt(KEY_ERROR_CODE, RemoteAuthClient.ERROR_UNSUPPORTED) }
)
}
}
diff --git a/wear/wear-remote-interactions/api/current.txt b/wear/wear-remote-interactions/api/current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/current.txt
+++ b/wear/wear-remote-interactions/api/current.txt
@@ -2,13 +2,17 @@
package androidx.wear.remote.interactions {
@RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
- method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
- field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+ method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
}
+ public static final class PlayStoreAvailability.Companion {
+ method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ }
+
public final class WatchFaceConfigIntentHelper {
method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/api/public_plus_experimental_current.txt b/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
+++ b/wear/wear-remote-interactions/api/public_plus_experimental_current.txt
@@ -2,13 +2,17 @@
package androidx.wear.remote.interactions {
@RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
- method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
- field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+ method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
}
+ public static final class PlayStoreAvailability.Companion {
+ method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ }
+
public final class WatchFaceConfigIntentHelper {
method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/api/restricted_current.txt b/wear/wear-remote-interactions/api/restricted_current.txt
index 0a4a745..367672c 100644
--- a/wear/wear-remote-interactions/api/restricted_current.txt
+++ b/wear/wear-remote-interactions/api/restricted_current.txt
@@ -2,13 +2,17 @@
package androidx.wear.remote.interactions {
@RequiresApi(android.os.Build.VERSION_CODES.N) public final class PlayStoreAvailability {
- method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
- field public static final androidx.wear.remote.interactions.PlayStoreAvailability INSTANCE;
+ method public static int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ field public static final androidx.wear.remote.interactions.PlayStoreAvailability.Companion Companion;
field public static final int PLAY_STORE_AVAILABLE = 1; // 0x1
field public static final int PLAY_STORE_ERROR_UNKNOWN = 0; // 0x0
field public static final int PLAY_STORE_UNAVAILABLE = 2; // 0x2
}
+ public static final class PlayStoreAvailability.Companion {
+ method public int getPlayStoreAvailabilityOnPhone(android.content.Context context);
+ }
+
public final class WatchFaceConfigIntentHelper {
method public static String? getPeerIdExtra(android.content.Intent watchFaceIntent);
method public static android.content.ComponentName? getWatchFaceComponentExtra(android.content.Intent watchFaceIntent);
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
index 02f04f9..70765b7 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/PlayStoreAvailability.kt
@@ -28,83 +28,87 @@
* Helper class for checking whether the phone paired to a given Wear OS device has the Play Store.
*/
@RequiresApi(Build.VERSION_CODES.N)
-public object PlayStoreAvailability {
- /**
- * This value means that there was an error in checking for whether the Play Store is available
- * on the phone.
- */
- public const val PLAY_STORE_ERROR_UNKNOWN: Int = 0
+public class PlayStoreAvailability private constructor() {
+ public companion object {
+ /**
+ * This value means that there was an error in checking for whether the Play Store is
+ * available son the phone.
+ */
+ public const val PLAY_STORE_ERROR_UNKNOWN: Int = 0
- /** This value means that the Play Store is available on the phone. */
- public const val PLAY_STORE_AVAILABLE: Int = 1
+ /** This value means that the Play Store is available on the phone. */
+ public const val PLAY_STORE_AVAILABLE: Int = 1
- /** This value means that the Play Store is not available on the phone. */
- public const val PLAY_STORE_UNAVAILABLE: Int = 2
+ /** This value means that the Play Store is not available on the phone. */
+ public const val PLAY_STORE_UNAVAILABLE: Int = 2
- private const val PLAY_STORE_AVAILABILITY_PATH = "play_store_availability"
- internal const val SETTINGS_AUTHORITY_URI = "com.google.android.wearable.settings"
- internal val PLAY_STORE_AVAILABILITY_URI = Uri.Builder()
- .scheme("content")
- .authority(SETTINGS_AUTHORITY_URI)
- .path(PLAY_STORE_AVAILABILITY_PATH)
- .build()
+ private const val PLAY_STORE_AVAILABILITY_PATH = "play_store_availability"
+ internal const val SETTINGS_AUTHORITY_URI = "com.google.android.wearable.settings"
+ internal val PLAY_STORE_AVAILABILITY_URI = Uri.Builder()
+ .scheme("content")
+ .authority(SETTINGS_AUTHORITY_URI)
+ .path(PLAY_STORE_AVAILABILITY_PATH)
+ .build()
- // The name of the row which stores the play store availability setting in versions before R.
- internal const val KEY_PLAY_STORE_AVAILABILITY = "play_store_availability"
+ // The name of the row which stores the play store availability setting in versions before
+ // R.
+ internal const val KEY_PLAY_STORE_AVAILABILITY = "play_store_availability"
- // The name of the settings value which stores the play store availability setting in versions
- // from R.
- private const val SETTINGS_PLAY_STORE_AVAILABILITY = "phone_play_store_availability"
+ // The name of the settings value which stores the play store availability setting in
+ // versions from R.
+ private const val SETTINGS_PLAY_STORE_AVAILABILITY = "phone_play_store_availability"
- internal const val SYSTEM_FEATURE_WATCH: String = "android.hardware.type.watch"
+ internal const val SYSTEM_FEATURE_WATCH: String = "android.hardware.type.watch"
- /**
- * Returns whether the Play Store is available on the Phone. If
- * [PLAY_STORE_ERROR_UNKNOWN] is returned, the caller should try again later. This
- * method should not be run on the main thread.
- *
- * @return One of three values: [PLAY_STORE_AVAILABLE],
- * [PLAY_STORE_UNAVAILABLE], or [PLAY_STORE_ERROR_UNKNOWN].
- */
- @PlayStoreStatus
- public fun getPlayStoreAvailabilityOnPhone(context: Context): Int {
- val isCurrentDeviceAWatch = context.packageManager.hasSystemFeature(
- SYSTEM_FEATURE_WATCH
- )
+ /**
+ * Returns whether the Play Store is available on the Phone. If
+ * [PLAY_STORE_ERROR_UNKNOWN] is returned, the caller should try again later. This
+ * method should not be run on the main thread.
+ *
+ * @return One of three values: [PLAY_STORE_AVAILABLE],
+ * [PLAY_STORE_UNAVAILABLE], or [PLAY_STORE_ERROR_UNKNOWN].
+ */
+ @JvmStatic
+ @PlayStoreStatus
+ public fun getPlayStoreAvailabilityOnPhone(context: Context): Int {
+ val isCurrentDeviceAWatch = context.packageManager.hasSystemFeature(
+ SYSTEM_FEATURE_WATCH
+ )
- if (!isCurrentDeviceAWatch) {
- val isPlayServiceAvailable =
- GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
- return if (isPlayServiceAvailable == ConnectionResult.SUCCESS) PLAY_STORE_AVAILABLE
- else PLAY_STORE_UNAVAILABLE
- }
+ if (!isCurrentDeviceAWatch) {
+ val isPlayServiceAvailable =
+ GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)
+ return if (isPlayServiceAvailable == ConnectionResult.SUCCESS) PLAY_STORE_AVAILABLE
+ else PLAY_STORE_UNAVAILABLE
+ }
- if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
- context.contentResolver.query(
- PLAY_STORE_AVAILABILITY_URI, null, null, null,
- null
- )?.use { cursor ->
- while (cursor.moveToNext()) {
- if (KEY_PLAY_STORE_AVAILABILITY == cursor.getString(0)) {
- return cursor.getInt(1)
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
+ context.contentResolver.query(
+ PLAY_STORE_AVAILABILITY_URI, null, null, null,
+ null
+ )?.use { cursor ->
+ while (cursor.moveToNext()) {
+ if (KEY_PLAY_STORE_AVAILABILITY == cursor.getString(0)) {
+ return cursor.getInt(1)
+ }
}
}
+ } else {
+ return Settings.Global.getInt(
+ context.contentResolver, SETTINGS_PLAY_STORE_AVAILABILITY,
+ PLAY_STORE_ERROR_UNKNOWN
+ )
}
- } else {
- return Settings.Global.getInt(
- context.contentResolver, SETTINGS_PLAY_STORE_AVAILABILITY,
- PLAY_STORE_ERROR_UNKNOWN
- )
+ return PLAY_STORE_ERROR_UNKNOWN
}
- return PLAY_STORE_ERROR_UNKNOWN
- }
- /** @hide */
- @IntDef(
- PLAY_STORE_ERROR_UNKNOWN,
- PLAY_STORE_AVAILABLE,
- PLAY_STORE_UNAVAILABLE
- )
- @Retention(AnnotationRetention.SOURCE)
- public annotation class PlayStoreStatus
+ /** @hide */
+ @IntDef(
+ PLAY_STORE_ERROR_UNKNOWN,
+ PLAY_STORE_AVAILABLE,
+ PLAY_STORE_UNAVAILABLE
+ )
+ @Retention(AnnotationRetention.SOURCE)
+ public annotation class PlayStoreStatus
+ }
}
\ No newline at end of file
diff --git a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
index 0e319da..2d669ae 100644
--- a/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
+++ b/wear/wear-remote-interactions/src/main/java/androidx/wear/remote/interactions/WatchFaceConfigIntentHelper.kt
@@ -32,32 +32,32 @@
* activity. The following meta-data will register the `com.example.watchface.CONFIG_DIGITAL`
* action to be started when configuring a watch face on the wearable device:
* ```
- * <meta-data
+ * <meta-data
* android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- * android:value="com.example.watchface.CONFIG_DIGITAL" />
+ * android:value="com.example.watchface.CONFIG_DIGITAL" />
* ```
*
*
* To register a configuration activity to be started on a companion phone, add the following
* alternative meta-data entry to the watch face component:
* ```
- * <meta-data
+ * <meta-data
* android:name="com.google.android.wearable.watchface.companionConfigurationAction"
- * android:value="com.example.watchface.CONFIG_DIGITAL" />
+ * android:value="com.example.watchface.CONFIG_DIGITAL" />
* ```
*
*
* The activity should have an intent filter which lists the action specified in the meta-data
* block above, in addition to the two categories present in the following example:
* ```
- * <activity android:name=".MyWatchFaceConfigActivity">
- * <intent-filter>
- * <action android:name="com.example.watchface.CONFIG_DIGITAL" />
- * <category android:name=
- * "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
- * <category android:name="android.intent.category.DEFAULT" />
- * </intent-filter>
- * </activity>
+ * <activity android:name=".MyWatchFaceConfigActivity">
+ * <intent-filter>
+ * <action android:name="com.example.watchface.CONFIG_DIGITAL" />
+ * <category android:name=
+ * "com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ * <category android:name="android.intent.category.DEFAULT" />
+ * </intent-filter>
+ * </activity>
* ```
*
*
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index 88201d9..8137255 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -37,77 +37,97 @@
property public final boolean hasLowBitAmbient;
}
- public interface EditorObserverListener {
- method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+ public interface EditorListener {
+ method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
public interface EditorServiceClient {
+ method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
method public void closeEditor();
- method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+ method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
}
public final class EditorState {
- method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
- method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
- method public String getWatchFaceInstanceId();
- method public boolean hasCommitChanges();
- property public final boolean commitChanges;
- property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
- property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
- property public final String watchFaceInstanceId;
+ method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+ method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+ method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+ method public boolean shouldCommitChanges();
+ property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+ property public final boolean shouldCommitChanges;
+ property public final androidx.wear.watchface.style.UserStyleData userStyle;
+ property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
}
public final class EditorStateKt {
}
public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+ method @AnyThread public boolean isConnectionAlive();
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public android.os.Bundle toBundle();
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
}
- public static final class HeadlessWatchFaceClient.Companion {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+ public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+ public static final class HeadlessWatchFaceClient.Companion {
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ }
+
+ public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public void displayPressedAnimation(int complicationId);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+ method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @AnyThread public boolean isConnectionAlive();
method public void performAmbientTick();
- method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
- method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+ method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+ method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+ property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
}
- public static final class InteractiveWatchFaceSysUiClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
- ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+ public static final class InteractiveWatchFaceClient.Companion {
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
+ }
+
+ public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+ ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
method public android.graphics.Rect getBounds();
method public android.app.PendingIntent? getTapAction();
method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -115,44 +135,12 @@
property public final android.app.PendingIntent? tapAction;
}
- public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public void bringAttentionToComplication(int complicationId);
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String getInstanceId();
- method public long getPreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
- method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
- method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String instanceId;
- property public abstract long previewReferenceTimeMillis;
- property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
- }
-
- public static final class InteractiveWatchFaceWcsClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- }
-
- public final class SystemState {
- ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
- method public int getInterruptionFilter();
- method public boolean inAmbientMode();
- property public final boolean inAmbientMode;
- property public final int interruptionFilter;
- }
-
public interface WatchFaceControlClient extends java.lang.AutoCloseable {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
}
@@ -168,5 +156,19 @@
ctor public WatchFaceControlClient.ServiceStartFailureException();
}
+ public final class WatchFaceId {
+ ctor public WatchFaceId(String id);
+ method public String getId();
+ property public final String id;
+ }
+
+ public final class WatchUiState {
+ ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+ method public int getInterruptionFilter();
+ method public boolean inAmbientMode();
+ property public final boolean inAmbientMode;
+ property public final int interruptionFilter;
+ }
+
}
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 07c20da..d04191e 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -37,77 +37,97 @@
property public final boolean hasLowBitAmbient;
}
- public interface EditorObserverListener {
- method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+ public interface EditorListener {
+ method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
public interface EditorServiceClient {
+ method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
method public void closeEditor();
- method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+ method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
}
public final class EditorState {
- method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
- method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
- method public String getWatchFaceInstanceId();
- method public boolean hasCommitChanges();
- property public final boolean commitChanges;
- property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
- property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
- property public final String watchFaceInstanceId;
+ method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+ method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+ method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+ method public boolean shouldCommitChanges();
+ property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+ property public final boolean shouldCommitChanges;
+ property public final androidx.wear.watchface.style.UserStyleData userStyle;
+ property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
}
public final class EditorStateKt {
}
public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+ method @AnyThread public boolean isConnectionAlive();
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public android.os.Bundle toBundle();
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
}
- public static final class HeadlessWatchFaceClient.Companion {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+ public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+ public static final class HeadlessWatchFaceClient.Companion {
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ }
+
+ public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public void displayPressedAnimation(int complicationId);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+ method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @AnyThread public boolean isConnectionAlive();
method public void performAmbientTick();
- method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
- method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+ method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+ method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+ property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
}
- public static final class InteractiveWatchFaceSysUiClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
- ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+ public static final class InteractiveWatchFaceClient.Companion {
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
+ }
+
+ public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+ ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
method public android.graphics.Rect getBounds();
method public android.app.PendingIntent? getTapAction();
method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -115,44 +135,12 @@
property public final android.app.PendingIntent? tapAction;
}
- public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public void bringAttentionToComplication(int complicationId);
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String getInstanceId();
- method public long getPreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
- method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
- method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String instanceId;
- property public abstract long previewReferenceTimeMillis;
- property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
- }
-
- public static final class InteractiveWatchFaceWcsClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- }
-
- public final class SystemState {
- ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
- method public int getInterruptionFilter();
- method public boolean inAmbientMode();
- property public final boolean inAmbientMode;
- property public final int interruptionFilter;
- }
-
public interface WatchFaceControlClient extends java.lang.AutoCloseable {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
}
@@ -168,5 +156,19 @@
ctor public WatchFaceControlClient.ServiceStartFailureException();
}
+ public final class WatchFaceId {
+ ctor public WatchFaceId(String id);
+ method public String getId();
+ property public final String id;
+ }
+
+ public final class WatchUiState {
+ ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+ method public int getInterruptionFilter();
+ method public boolean inAmbientMode();
+ property public final boolean inAmbientMode;
+ property public final int interruptionFilter;
+ }
+
}
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 604dab1..bce2031 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -38,26 +38,25 @@
property public final boolean hasLowBitAmbient;
}
- public interface EditorObserverListener {
- method public void onEditorStateChange(androidx.wear.watchface.client.EditorState editorState);
+ public interface EditorListener {
+ method public void onEditorStateChanged(androidx.wear.watchface.client.EditorState editorState);
}
public interface EditorServiceClient {
+ method public void addListener(androidx.wear.watchface.client.EditorListener editorListener, java.util.concurrent.Executor listenerExecutor);
method public void closeEditor();
- method public void registerObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void registerObserver(optional java.util.concurrent.Executor? observerCallbackExecutor, androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
- method public void unregisterObserver(androidx.wear.watchface.client.EditorObserverListener editorObserverListener);
+ method public void removeListener(androidx.wear.watchface.client.EditorListener editorListener);
}
public final class EditorState {
- method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationData();
- method public java.util.Map<java.lang.String,java.lang.String> getUserStyle();
- method public String getWatchFaceInstanceId();
- method public boolean hasCommitChanges();
- property public final boolean commitChanges;
- property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationData;
- property public final java.util.Map<java.lang.String,java.lang.String> userStyle;
- property public final String watchFaceInstanceId;
+ method public java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> getPreviewComplicationsData();
+ method public androidx.wear.watchface.style.UserStyleData getUserStyle();
+ method public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
+ method public boolean shouldCommitChanges();
+ property public final java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData> previewComplicationsData;
+ property public final boolean shouldCommitChanges;
+ property public final androidx.wear.watchface.style.UserStyleData userStyle;
+ property public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
}
public final class EditorStateKt {
@@ -65,51 +64,72 @@
}
public interface HeadlessWatchFaceClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public default static androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
+ method @AnyThread public boolean isConnectionAlive();
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.HeadlessWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap? renderComplicationToBitmap(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public android.os.Bundle toBundle();
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final String BINDER_KEY = "HeadlessWatchFaceClient";
field public static final androidx.wear.watchface.client.HeadlessWatchFaceClient.Companion Companion;
}
- public static final class HeadlessWatchFaceClient.Companion {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBinder(android.os.IBinder binder);
+ public static interface HeadlessWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public interface InteractiveWatchFaceSysUiClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> getContentDescriptionLabels();
+ public static final class HeadlessWatchFaceClient.Companion {
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient createFromBundle(android.os.Bundle bundle);
+ }
+
+ public interface InteractiveWatchFaceClient extends java.lang.AutoCloseable {
+ method @AnyThread public void addClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener, java.util.concurrent.Executor executor);
+ method public void displayPressedAnimation(int complicationId);
+ method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
+ method public java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> getContentDescriptionLabels();
method public String getInstanceId();
method public long getPreviewReferenceTimeMillis();
+ method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+ method @AnyThread public boolean isConnectionAlive();
method public void performAmbientTick();
- method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
- method public void setSystemState(androidx.wear.watchface.client.SystemState systemState);
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
+ method @AnyThread public void removeClientDisconnectListener(androidx.wear.watchface.client.InteractiveWatchFaceClient.ClientDisconnectListener listener);
+ method @RequiresApi(27) public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+ method public void sendTouchEvent(@Px int xPosition, @Px int yPosition, @androidx.wear.watchface.TapType int tapType);
+ method public void setWatchUiState(androidx.wear.watchface.client.WatchUiState watchUiState);
+ method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
+ method public void updateWatchFaceInstance(String newInstanceId, androidx.wear.watchface.style.UserStyleData userStyle);
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
+ property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceClient.ContentDescriptionLabel> contentDescriptionLabels;
property public abstract String instanceId;
property public abstract long previewReferenceTimeMillis;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.Companion Companion;
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
+ field public static final androidx.wear.watchface.client.InteractiveWatchFaceClient.Companion Companion;
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
}
- public static final class InteractiveWatchFaceSysUiClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient createFromBinder(android.os.IBinder binder);
- field public static final int TAP_TYPE_TAP = 2; // 0x2
- field public static final int TAP_TYPE_TOUCH = 0; // 0x0
- field public static final int TAP_TYPE_TOUCH_CANCEL = 1; // 0x1
+ public static interface InteractiveWatchFaceClient.ClientDisconnectListener {
+ method public void onClientDisconnected();
}
- public static final class InteractiveWatchFaceSysUiClient.ContentDescriptionLabel {
- ctor public InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(android.support.wearable.complications.TimeDependentText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
+ public static final class InteractiveWatchFaceClient.Companion {
+ field public static final int TAP_TYPE_CANCEL = 1; // 0x1
+ field public static final int TAP_TYPE_DOWN = 0; // 0x0
+ field public static final int TAP_TYPE_UP = 2; // 0x2
+ }
+
+ public static final class InteractiveWatchFaceClient.ContentDescriptionLabel {
+ ctor public InteractiveWatchFaceClient.ContentDescriptionLabel(androidx.wear.complications.data.ComplicationText text, android.graphics.Rect bounds, android.app.PendingIntent? tapAction);
method public android.graphics.Rect getBounds();
method public android.app.PendingIntent? getTapAction();
method public CharSequence getTextAt(android.content.res.Resources resources, long dateTimeMillis);
@@ -117,47 +137,12 @@
property public final android.app.PendingIntent? tapAction;
}
- public interface InteractiveWatchFaceWcsClient extends java.lang.AutoCloseable {
- method public android.os.IBinder asBinder();
- method public void bringAttentionToComplication(int complicationId);
- method public default static androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- method public default Integer? getComplicationIdAt(@Px int x, @Px int y);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String getInstanceId();
- method public long getPreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
- method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
- method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
- method public void updateInstance(String newInstanceId, androidx.wear.watchface.style.UserStyle userStyle);
- method public void updateInstance(String newInstanceId, java.util.Map<java.lang.String,java.lang.String> userStyle);
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String instanceId;
- property public abstract long previewReferenceTimeMillis;
- property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
- field public static final androidx.wear.watchface.client.InteractiveWatchFaceWcsClient.Companion Companion;
- }
-
- public static final class InteractiveWatchFaceWcsClient.Companion {
- method public androidx.wear.watchface.client.InteractiveWatchFaceWcsClient createFromBinder(android.os.IBinder binder);
- }
-
- public final class SystemState {
- ctor public SystemState(boolean inAmbientMode, int interruptionFilter);
- method public int getInterruptionFilter();
- method public boolean inAmbientMode();
- property public final boolean inAmbientMode;
- property public final int interruptionFilter;
- }
-
- @IntDef({androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH, androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH_CANCEL, androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.TAP_TYPE_TAP}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public @interface TapType {
- }
-
public interface WatchFaceControlClient extends java.lang.AutoCloseable {
- method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
+ method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, @Px int surfaceWidth, @Px int surfaceHeight);
method public default static suspend Object? createWatchFaceControlClient(android.content.Context p, String context, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.WatchFaceControlClient> watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> p);
field public static final androidx.wear.watchface.client.WatchFaceControlClient.Companion Companion;
}
@@ -173,5 +158,19 @@
ctor public WatchFaceControlClient.ServiceStartFailureException();
}
+ public final class WatchFaceId {
+ ctor public WatchFaceId(String id);
+ method public String getId();
+ property public final String id;
+ }
+
+ public final class WatchUiState {
+ ctor public WatchUiState(boolean inAmbientMode, int interruptionFilter);
+ method public int getInterruptionFilter();
+ method public boolean inAmbientMode();
+ property public final boolean inAmbientMode;
+ property public final int interruptionFilter;
+ }
+
}
diff --git a/wear/wear-watchface-client/guava/api/current.txt b/wear/wear-watchface-client/guava/api/current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/current.txt
+++ b/wear/wear-watchface-client/guava/api/current.txt
@@ -7,9 +7,9 @@
method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
}
diff --git a/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/guava/api/public_plus_experimental_current.txt
@@ -7,9 +7,9 @@
method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
}
diff --git a/wear/wear-watchface-client/guava/api/restricted_current.txt b/wear/wear-watchface-client/guava/api/restricted_current.txt
index 2636cf73..bf268ec 100644
--- a/wear/wear-watchface-client/guava/api/restricted_current.txt
+++ b/wear/wear-watchface-client/guava/api/restricted_current.txt
@@ -7,9 +7,9 @@
method public androidx.wear.watchface.client.HeadlessWatchFaceClient? createHeadlessWatchFaceClient(android.content.ComponentName watchFaceName, androidx.wear.watchface.client.DeviceConfig deviceConfig, int surfaceWidth, int surfaceHeight);
method public static final com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.ListenableWatchFaceControlClient> createWatchFaceControlClient(android.content.Context context, String watchFacePackageName);
method public androidx.wear.watchface.client.EditorServiceClient getEditorServiceClient();
- method public androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient? getInteractiveWatchFaceSysUiClientInstance(String instanceId);
- method public kotlinx.coroutines.Deferred<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
- method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceWcsClient> listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.SystemState systemState, java.util.Map<java.lang.String,java.lang.String>? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+ method public androidx.wear.watchface.client.InteractiveWatchFaceClient? getInteractiveWatchFaceClientInstance(String instanceId);
+ method public suspend Object? getOrCreateInteractiveWatchFaceClient(String p, androidx.wear.watchface.client.DeviceConfig p1, androidx.wear.watchface.client.WatchUiState p2, androidx.wear.watchface.style.UserStyleData? p3, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? p4, kotlin.coroutines.Continuation<? super androidx.wear.watchface.client.InteractiveWatchFaceClient> $completion);
+ method public com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.client.InteractiveWatchFaceClient> listenableGetOrCreateInteractiveWatchFaceClient(String id, androidx.wear.watchface.client.DeviceConfig deviceConfig, androidx.wear.watchface.client.WatchUiState watchUiState, androidx.wear.watchface.style.UserStyleData? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
field public static final androidx.wear.watchface.client.ListenableWatchFaceControlClient.Companion Companion;
}
diff --git a/wear/wear-watchface-client/guava/build.gradle b/wear/wear-watchface-client/guava/build.gradle
index 43b465a..3c0a1a5 100644
--- a/wear/wear-watchface-client/guava/build.gradle
+++ b/wear/wear-watchface-client/guava/build.gradle
@@ -46,7 +46,7 @@
name = "Android Wear Watchface Client Guava"
type = LibraryType.PUBLISHED_LIBRARY
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_WATCHFACE_CLIENT
+ mavenVersion = LibraryVersions.WEAR_WATCHFACE_CLIENT_GUAVA
inceptionYear = "2021"
description = "Guava wrappers for the Androidx Wear Watchface library"
}
diff --git a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
index 12aa604..fbb2ad3 100644
--- a/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/guava/src/androidTest/java/androidx/wear/watchface/ListenableWatchFaceControlClientTest.kt
@@ -31,7 +31,7 @@
import androidx.test.filters.MediumTest
import androidx.wear.watchface.client.DeviceConfig
import androidx.wear.watchface.client.ListenableWatchFaceControlClient
-import androidx.wear.watchface.client.SystemState
+import androidx.wear.watchface.client.WatchUiState
import androidx.wear.watchface.control.WatchFaceControlServiceFactory
import androidx.wear.watchface.samples.createExampleCanvasAnalogWatchFaceBuilder
import com.google.common.truth.Truth.assertThat
@@ -56,7 +56,7 @@
private lateinit var surface: Surface
@Before
- fun setUp() {
+ public fun setUp() {
MockitoAnnotations.initMocks(this)
Mockito.`when`(surfaceHolder.surfaceFrame)
.thenReturn(Rect(0, 0, 400, 400))
@@ -83,7 +83,7 @@
400
)!!
- assertThat(headlessInstance.userStyleSchema.userStyleSettings.map { it.id })
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings.map { it.id.value })
.containsExactly(
"color_style_setting",
"draw_hour_pips_style_setting",
@@ -126,7 +126,7 @@
).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
val interactiveInstanceFuture =
- client.listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+ client.listenableGetOrCreateInteractiveWatchFaceClient(
"listenableTestId",
DeviceConfig(
false,
@@ -134,7 +134,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
null,
null
)
@@ -152,7 +152,7 @@
)
val interactiveInstance = interactiveInstanceFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
- assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id })
+ assertThat(interactiveInstance.userStyleSchema.userStyleSettings.map { it.id.value })
.containsExactly(
"color_style_setting",
"draw_hour_pips_style_setting",
@@ -221,7 +221,7 @@
).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
val interactiveInstanceFuture =
- client.listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+ client.listenableGetOrCreateInteractiveWatchFaceClient(
"listenableTestId",
DeviceConfig(
false,
@@ -229,7 +229,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
null,
null
)
@@ -271,7 +271,7 @@
context.packageName
).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
- assertNull(client.getInteractiveWatchFaceSysUiClientInstance("I do not exist"))
+ assertNull(client.getInteractiveWatchFaceClientInstance("I do not exist"))
}
}
diff --git a/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt b/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
index 3857f023..112669c 100644
--- a/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/guava/src/main/java/androidx/wear/watchface/client/ListenableWatchFaceControlClient.kt
@@ -23,10 +23,12 @@
import androidx.wear.complications.data.ComplicationData
import androidx.wear.utility.AsyncTraceEvent
import androidx.wear.watchface.client.WatchFaceControlClient.ServiceNotBoundException
+import androidx.wear.watchface.style.UserStyleData
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.async
import kotlinx.coroutines.guava.asListenableFuture
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
@@ -38,12 +40,20 @@
public open class ListenableWatchFaceControlClient(
private val watchFaceControlClient: WatchFaceControlClient
) : WatchFaceControlClient {
- override fun getInteractiveWatchFaceSysUiClientInstance(
+ override fun getInteractiveWatchFaceClientInstance(
instanceId: String
- ): InteractiveWatchFaceSysUiClient? =
- watchFaceControlClient.getInteractiveWatchFaceSysUiClientInstance(instanceId)
+ ): InteractiveWatchFaceClient? =
+ watchFaceControlClient.getInteractiveWatchFaceClientInstance(instanceId)
public companion object {
+ private val immediateCoroutineScope = CoroutineScope(
+ object : CoroutineDispatcher() {
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ block.run()
+ }
+ }
+ )
+
/**
* Returns a [ListenableFuture] for a [ListenableWatchFaceControlClient] which attempts to
* connect to a watch face in the android package [watchFacePackageName].
@@ -62,13 +72,7 @@
"ListenableWatchFaceControlClient.createWatchFaceControlClient"
)
val future = ResolvableFuture.create<ListenableWatchFaceControlClient>()
- val coroutineScope =
- CoroutineScope(object : CoroutineDispatcher() {
- override fun dispatch(context: CoroutineContext, block: Runnable) {
- block.run()
- }
- })
- coroutineScope.launch {
+ immediateCoroutineScope.launch {
try {
future.set(
ListenableWatchFaceControlClient(
@@ -103,35 +107,36 @@
/**
* [ListenableFuture] wrapper around
- * [WatchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync].
+ * [WatchFaceControlClient.getOrCreateInteractiveWatchFaceClient].
* This is open to allow mocking.
*/
- public open fun listenableGetOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+ public open fun listenableGetOrCreateInteractiveWatchFaceClient(
id: String,
deviceConfig: DeviceConfig,
- systemState: SystemState,
- userStyle: Map<String, String>?,
+ watchUiState: WatchUiState,
+ userStyle: UserStyleData?,
idToComplicationData: Map<Int, ComplicationData>?
- ): ListenableFuture<InteractiveWatchFaceWcsClient> =
- watchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ ): ListenableFuture<InteractiveWatchFaceClient> = immediateCoroutineScope.async {
+ watchFaceControlClient.getOrCreateInteractiveWatchFaceClient(
id,
deviceConfig,
- systemState,
+ watchUiState,
userStyle,
idToComplicationData
- ).asListenableFuture()
+ )
+ }.asListenableFuture()
- override fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ override suspend fun getOrCreateInteractiveWatchFaceClient(
id: String,
deviceConfig: DeviceConfig,
- systemState: SystemState,
- userStyle: Map<String, String>?,
+ watchUiState: WatchUiState,
+ userStyle: UserStyleData?,
idToComplicationData: Map<Int, ComplicationData>?
- ): Deferred<InteractiveWatchFaceWcsClient> =
- watchFaceControlClient.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ ): InteractiveWatchFaceClient =
+ watchFaceControlClient.getOrCreateInteractiveWatchFaceClient(
id,
deviceConfig,
- systemState,
+ watchUiState,
userStyle,
idToComplicationData
)
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
index 7e58fd9..cf35008 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/EditorServiceClientTest.kt
@@ -18,7 +18,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import androidx.wear.watchface.client.EditorObserverListener
+import androidx.wear.watchface.client.EditorListener
import androidx.wear.watchface.client.EditorServiceClientImpl
import androidx.wear.watchface.client.EditorState
import androidx.wear.watchface.editor.EditorService
@@ -37,27 +37,37 @@
@Test
fun registerObserver() {
lateinit var observedEditorState: EditorState
- val observer = object : EditorObserverListener {
- override fun onEditorStateChange(editorState: EditorState) {
+ val observer = object : EditorListener {
+ override fun onEditorStateChanged(editorState: EditorState) {
observedEditorState = editorState
}
}
- editorServiceClient.registerObserver(observer)
+ editorServiceClient.addListener(observer) { runnable -> runnable.run() }
val watchFaceInstanceId = "id-1"
EditorService.globalEditorService.broadcastEditorState(
EditorStateWireFormat(
watchFaceInstanceId,
- UserStyleWireFormat(mapOf("color" to "red", "size" to "small")),
+ UserStyleWireFormat(
+ mapOf(
+ "color" to "red".encodeToByteArray(),
+ "size" to "small".encodeToByteArray()
+ )
+ ),
emptyList(),
true
)
)
- editorServiceClient.unregisterObserver(observer)
+ editorServiceClient.removeListener(observer)
- assertThat(observedEditorState.watchFaceInstanceId).isEqualTo(watchFaceInstanceId)
+ assertThat(observedEditorState.watchFaceId.id).isEqualTo(watchFaceInstanceId)
assertThat(observedEditorState.userStyle.toString()).isEqualTo("{color=red, size=small}")
- assertTrue(observedEditorState.commitChanges)
+ assertTrue(observedEditorState.shouldCommitChanges)
+
+ val editorStateString = observedEditorState.toString()
+ assertThat(editorStateString).contains("watchFaceId: $watchFaceInstanceId")
+ assertThat(editorStateString).contains("{color=red, size=small}")
+ assertThat(editorStateString).contains("shouldCommitChanges: true")
}
}
\ No newline at end of file
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 6548a2e..bfb35dd 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -40,8 +40,9 @@
import androidx.wear.watchface.LayerMode
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.client.DeviceConfig
-import androidx.wear.watchface.client.SystemState
+import androidx.wear.watchface.client.HeadlessWatchFaceClient
import androidx.wear.watchface.client.WatchFaceControlClient
+import androidx.wear.watchface.client.WatchUiState
import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.samples.BLUE_STYLE
@@ -55,7 +56,11 @@
import androidx.wear.watchface.samples.NO_COMPLICATIONS
import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
import androidx.wear.watchface.style.Layer
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption
+import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
+import androidx.wear.watchface.style.UserStyleData
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.After
@@ -76,7 +81,7 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
-class WatchFaceControlClientTest {
+public class WatchFaceControlClientTest {
private val context = ApplicationProvider.getApplicationContext<Context>()
private val service = runBlocking {
WatchFaceControlClient.createWatchFaceControlClientImpl(
@@ -89,6 +94,7 @@
@Mock
private lateinit var surfaceHolder: SurfaceHolder
+
@Mock
private lateinit var surface: Surface
private lateinit var engine: WallpaperService.Engine
@@ -97,7 +103,7 @@
private lateinit var wallpaperService: TestExampleCanvasAnalogWatchFaceService
@Before
- fun setUp() {
+ public fun setUp() {
MockitoAnnotations.initMocks(this)
wallpaperService = TestExampleCanvasAnalogWatchFaceService(context, surfaceHolder)
@@ -107,7 +113,7 @@
}
@After
- fun tearDown() {
+ public fun tearDown() {
if (this::engine.isInitialized) {
engine.onDestroy()
}
@@ -115,7 +121,8 @@
}
@get:Rule
- val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface-client")
+ public val screenshotRule: AndroidXScreenshotTestRule =
+ AndroidXScreenshotTestRule("wear/wear-watchface-client")
private val exampleWatchFaceComponentName = ComponentName(
"androidx.wear.watchface.samples.test",
@@ -129,7 +136,7 @@
0
)
- private val systemState = SystemState(false, 0)
+ private val systemState = WatchUiState(false, 0)
private val complications = mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
@@ -157,7 +164,7 @@
}
@Test
- fun headlessScreenshot() {
+ public fun headlessScreenshot() {
val headlessInstance = service.createHeadlessWatchFaceClient(
ComponentName(
"androidx.wear.watchface.samples.test",
@@ -172,7 +179,7 @@
400,
400
)!!
- val bitmap = headlessInstance.takeWatchFaceScreenshot(
+ val bitmap = headlessInstance.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -190,7 +197,7 @@
}
@Test
- fun yellowComplicationHighlights() {
+ public fun yellowComplicationHighlights() {
val headlessInstance = service.createHeadlessWatchFaceClient(
ComponentName(
"androidx.wear.watchface.samples.test",
@@ -205,13 +212,13 @@
400,
400
)!!
- val bitmap = headlessInstance.takeWatchFaceScreenshot(
+ val bitmap = headlessInstance.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
null,
Color.YELLOW
@@ -227,7 +234,7 @@
}
@Test
- fun headlessComplicationDetails() {
+ public fun headlessComplicationDetails() {
val headlessInstance = service.createHeadlessWatchFaceClient(
exampleWatchFaceComponentName,
deviceConfig,
@@ -235,9 +242,9 @@
400
)!!
- assertThat(headlessInstance.complicationState.size).isEqualTo(2)
+ assertThat(headlessInstance.complicationsState.size).isEqualTo(2)
- val leftComplicationDetails = headlessInstance.complicationState[
+ val leftComplicationDetails = headlessInstance.complicationsState[
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
]!!
assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -257,7 +264,7 @@
)
assertTrue(leftComplicationDetails.isEnabled)
- val rightComplicationDetails = headlessInstance.complicationState[
+ val rightComplicationDetails = headlessInstance.complicationsState[
EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
]!!
assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
@@ -281,7 +288,7 @@
}
@Test
- fun headlessUserStyleSchema() {
+ public fun headlessUserStyleSchema() {
val headlessInstance = service.createHeadlessWatchFaceClient(
exampleWatchFaceComponentName,
deviceConfig,
@@ -290,16 +297,16 @@
)!!
assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(4)
- assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id).isEqualTo(
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings[0].id.value).isEqualTo(
"color_style_setting"
)
- assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id).isEqualTo(
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings[1].id.value).isEqualTo(
"draw_hour_pips_style_setting"
)
- assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id).isEqualTo(
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings[2].id.value).isEqualTo(
"watch_hand_length_style_setting"
)
- assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id).isEqualTo(
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings[3].id.value).isEqualTo(
"complications_style_setting"
)
@@ -307,27 +314,39 @@
}
@Test
- fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun headlessToBundleAndCreateFromBundle() {
+ val headlessInstance = HeadlessWatchFaceClient.createFromBundle(
+ service.createHeadlessWatchFaceClient(
+ exampleWatchFaceComponentName,
+ deviceConfig,
+ 400,
+ 400
+ )!!.toBundle()
+ )
+
+ assertThat(headlessInstance.userStyleSchema.userStyleSettings.size).isEqualTo(4)
+ }
+
+ @Test
+ public fun getOrCreateInteractiveWatchFaceClient(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
- val interactiveInstance =
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
- }
+ val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
+ }
- val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+ val bitmap = interactiveInstance.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -347,32 +366,32 @@
}
@Test
- fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_initialStyle() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun getOrCreateInteractiveWatchFaceClient_initialStyle(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
// An incomplete map which is OK.
- mapOf(
- "color_style_setting" to "green_style",
- "draw_hour_pips_style_setting" to "false",
- "watch_hand_length_style_setting" to "0.8"
+ UserStyleData(
+ mapOf(
+ "color_style_setting" to "green_style".encodeToByteArray(),
+ "draw_hour_pips_style_setting" to BooleanOption(false).id.value,
+ "watch_hand_length_style_setting" to DoubleRangeOption(0.8).id.value
+ )
),
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
- val interactiveInstance =
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
- }
+ val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
+ }
- val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+ val bitmap = interactiveInstance.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -392,25 +411,23 @@
}
@Test
- fun wallpaperServiceBackedInteractiveWatchFaceWcsClient_ComplicationDetails() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun interactiveWatchFaceClient_ComplicationDetails(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
- val interactiveInstance =
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
- }
+ val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
+ }
interactiveInstance.updateComplicationData(
mapOf(
@@ -425,9 +442,9 @@
)
)
- assertThat(interactiveInstance.complicationState.size).isEqualTo(2)
+ assertThat(interactiveInstance.complicationsState.size).isEqualTo(2)
- val leftComplicationDetails = interactiveInstance.complicationState[
+ val leftComplicationDetails = interactiveInstance.complicationsState[
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
]!!
assertThat(leftComplicationDetails.bounds).isEqualTo(Rect(80, 160, 160, 240))
@@ -450,7 +467,7 @@
ComplicationType.SHORT_TEXT
)
- val rightComplicationDetails = interactiveInstance.complicationState[
+ val rightComplicationDetails = interactiveInstance.complicationsState[
EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
]!!
assertThat(rightComplicationDetails.bounds).isEqualTo(Rect(240, 160, 320, 240))
@@ -477,72 +494,68 @@
}
@Test
- fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_existingOpenInstance() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun getOrCreateInteractiveWatchFaceClient_existingOpenInstance(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
-
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
-
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
}
- val existingInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
+
+ withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
+ }
+
+ withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
-
- assertTrue(existingInstance.isCompleted)
+ }
}
@Test
- fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient_existingClosedInstance() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun getOrCreateInteractiveWatchFaceClient_existingClosedInstance(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
// Wait for the instance to be created.
- val interactiveInstance =
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
- }
-
+ val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
+ }
// Closing this interface means the subsequent
- // getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient won't immediately return
+ // getOrCreateInteractiveWatchFaceClient won't immediately return
// a resolved future.
interactiveInstance.close()
- val deferredExistingInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ val deferredExistingInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
assertFalse(deferredExistingInstance.isCompleted)
@@ -556,36 +569,34 @@
surfaceHolder.surfaceFrame.height()
)
}
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredExistingInstance.await()
- }
+
+ withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredExistingInstance.await()
}
}
@Test
- fun getInteractiveWatchFaceInstanceSysUi() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun getInteractiveWatchFaceInstance(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
// Wait for the instance to be created.
- runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
+ withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
}
val sysUiInterface =
- service.getInteractiveWatchFaceSysUiClientInstance("testId")!!
+ service.getInteractiveWatchFaceClientInstance("testId")!!
val contentDescriptionLabels = sysUiInterface.contentDescriptionLabels
assertThat(contentDescriptionLabels.size).isEqualTo(3)
@@ -608,23 +619,26 @@
}
@Test
- fun updateInstance() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun updateInstance(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
- mapOf(
- COLOR_STYLE_SETTING to GREEN_STYLE,
- WATCH_HAND_LENGTH_STYLE_SETTING to "0.25",
- DRAW_HOUR_PIPS_STYLE_SETTING to "false",
- COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS
+ UserStyleData(
+ mapOf(
+ COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray(),
+ WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.25).id.value,
+ DRAW_HOUR_PIPS_STYLE_SETTING to BooleanOption(false).id.value,
+ COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS.encodeToByteArray()
+ )
),
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
// Wait for the instance to be created.
val interactiveInstance = runBlocking {
@@ -637,11 +651,13 @@
// Note this map doesn't include all the categories, which is fine the others will be set
// to their defaults.
- interactiveInstance.updateInstance(
+ interactiveInstance.updateWatchFaceInstance(
"testId2",
- mapOf(
- COLOR_STYLE_SETTING to BLUE_STYLE,
- WATCH_HAND_LENGTH_STYLE_SETTING to "0.9",
+ UserStyleData(
+ mapOf(
+ COLOR_STYLE_SETTING to BLUE_STYLE.encodeToByteArray(),
+ WATCH_HAND_LENGTH_STYLE_SETTING to DoubleRangeOption(0.9).id.value,
+ )
)
)
@@ -649,20 +665,20 @@
// The complications should have been cleared.
val leftComplication =
- interactiveInstance.complicationState[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
+ interactiveInstance.complicationsState[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
val rightComplication =
- interactiveInstance.complicationState[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
+ interactiveInstance.complicationsState[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
assertThat(leftComplication.currentType).isEqualTo(ComplicationType.NO_DATA)
assertThat(rightComplication.currentType).isEqualTo(ComplicationType.NO_DATA)
- // It should be possible to create a SysUI instance with the updated id.
- val sysUiInterface =
- service.getInteractiveWatchFaceSysUiClientInstance("testId2")
- assertThat(sysUiInterface).isNotNull()
- sysUiInterface?.close()
+ // It should be possible to create an instance with the updated id.
+ val instance =
+ service.getInteractiveWatchFaceClientInstance("testId2")
+ assertThat(instance).isNotNull()
+ instance?.close()
interactiveInstance.updateComplicationData(complications)
- val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+ val bitmap = interactiveInstance.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -683,23 +699,22 @@
}
@Test
- fun getComplicationIdAt() {
- val deferredInteractiveInstance =
- service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public fun getComplicationIdAt(): Unit = runBlocking {
+ val deferredInteractiveInstance = async {
+ service.getOrCreateInteractiveWatchFaceClient(
"testId",
deviceConfig,
systemState,
null,
complications
)
+ }
- // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
- createEngine()
+ // Create the engine which triggers creation of InteractiveWatchFaceClient.
+ async { createEngine() }
- val interactiveInstance = runBlocking {
- withTimeout(CONNECT_TIMEOUT_MILLIS) {
- deferredInteractiveInstance.await()
- }
+ val interactiveInstance = withTimeout(CONNECT_TIMEOUT_MILLIS) {
+ deferredInteractiveInstance.await()
}
assertNull(interactiveInstance.getComplicationIdAt(0, 0))
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
index 802461f..c48e5da 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorServiceClient.kt
@@ -22,36 +22,30 @@
import androidx.wear.watchface.editor.data.EditorStateWireFormat
import java.util.concurrent.Executor
-/** Client for the watchface editor service. */
+/**
+ * Client for the watchface editor service, which observes
+ * [androidx.wear.watchface.editor.EditorSession]. This client can be reused to observe multiple
+ * editor sessions.
+ */
public interface EditorServiceClient {
/**
- * Starts listening to [androidx.wear.watchface.editor.EditorSession] events, with the callback
- * executed by an immediate executor on an undefined thread.
+ * Starts listening for [EditorState] which is sent when
+ * [androidx.wear.watchface.editor.EditorSession] closes. The
+ * [EditorListener.onEditorStateChanged] callback is run on the specified [listenerExecutor].
*/
- public fun registerObserver(
- editorObserverListener: EditorObserverListener
- )
+ public fun addListener(editorListener: EditorListener, listenerExecutor: Executor)
- /**
- * Starts listening to [androidx.wear.watchface.editor.EditorSession] events with the callback
- * run on the specified [Executor] by an immediate executor, on an undefined thread if `null`.
- */
- public fun registerObserver(
- observerCallbackExecutor: Executor? = null,
- editorObserverListener: EditorObserverListener
- )
-
- /** Unregisters an [EditorObserverListener] previously registered via [registerObserver]. */
- public fun unregisterObserver(editorObserverListener: EditorObserverListener)
+ /** Unregisters an [EditorListener] previously registered via [addListener]. */
+ public fun removeListener(editorListener: EditorListener)
/** Instructs any open editor to close. */
public fun closeEditor()
}
/** Observes state changes in [androidx.wear.watchface.editor.EditorSession]. */
-public interface EditorObserverListener {
+public interface EditorListener {
/** Called in response to [androidx.wear.watchface.editor.EditorSession.close] .*/
- public fun onEditorStateChange(editorState: EditorState)
+ public fun onEditorStateChanged(editorState: EditorState)
}
/** @hide */
@@ -60,28 +54,18 @@
private val iEditorService: IEditorService
) : EditorServiceClient {
private val lock = Any()
- private val editorMap = HashMap<EditorObserverListener, Int>()
+ private val editorMap = HashMap<EditorListener, Int>()
- override fun registerObserver(editorObserverListener: EditorObserverListener) {
- registerObserver(null, editorObserverListener)
- }
-
- override fun registerObserver(
- observerCallbackExecutor: Executor?,
- editorObserverListener: EditorObserverListener
+ override fun addListener(
+ editorListener: EditorListener,
+ listenerExecutor: Executor
) {
- val executor = observerCallbackExecutor ?: object : Executor {
- override fun execute(runnable: Runnable) {
- runnable.run()
- }
- }
-
val observer = object : IEditorObserver.Stub() {
override fun getApiVersion() = IEditorObserver.API_VERSION
override fun onEditorStateChange(editorStateWireFormat: EditorStateWireFormat) {
- executor.execute {
- editorObserverListener.onEditorStateChange(
+ listenerExecutor.execute {
+ editorListener.onEditorStateChanged(
editorStateWireFormat.asApiEditorState()
)
}
@@ -89,15 +73,15 @@
}
synchronized(lock) {
- editorMap[editorObserverListener] = iEditorService.registerObserver(observer)
+ editorMap[editorListener] = iEditorService.registerObserver(observer)
}
}
- override fun unregisterObserver(editorObserverListener: EditorObserverListener) {
+ override fun removeListener(editorListener: EditorListener) {
synchronized(lock) {
- editorMap[editorObserverListener]?.let {
+ editorMap[editorListener]?.let {
iEditorService.unregisterObserver(it)
- editorMap.remove(editorObserverListener)
+ editorMap.remove(editorListener)
}
}
}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
index 4d8b8fd..339e657 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/EditorState.kt
@@ -20,42 +20,60 @@
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.wear.complications.data.ComplicationData
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.watchface.editor.data.EditorStateWireFormat
import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
+
+/**
+ * The system is responsible for the management and generation of these ids and they have no
+ * context outside of an instance of an EditorState and should not be stored or saved for later
+ * use by the WatchFace provider.
+ *
+ * @param id The system's id for a watch face being edited. This is passed in from
+ * [androidx.wear.watchface.EditorRequest.watchFaceId].
+ */
+public class WatchFaceId(public val id: String)
/**
* The state of the editing session. See [androidx.wear.watchface.editor.EditorSession].
*
- * @param watchFaceInstanceId Unique ID for the instance of the watch face being edited, only
- * defined for Android R and beyond.
- * @param userStyle The current [UserStyle] encoded as a Map<String, String>.
- * @param previewComplicationData Preview [ComplicationData] needed for taking screenshots without
+ * @param watchFaceId Unique ID for the instance of the watch face being edited (see
+ * [androidx.wear.watchface.editor.EditorRequest.watchFaceId]), only defined for
+ * Android R and beyond.
+ * @param userStyle The current [UserStyle] encoded as a [UserStyleData].
+ * @param previewComplicationsData Preview [ComplicationData] needed for taking screenshots without
* live complication data.
- * @param commitChanges Whether or not this state should be committed (i.e. the user aborted the
- * session). If it's not committed then any changes (E.g. complication provider changes)
+ * @param shouldCommitChanges Whether or not this state should be committed (i.e. the user aborted
+ * the session). If it's not committed then any changes (E.g. complication provider changes)
* should be abandoned. There's no need to resend the style to the watchface because the
* library will have restored the previous style.
*/
public class EditorState internal constructor(
@RequiresApi(Build.VERSION_CODES.R)
- public val watchFaceInstanceId: String,
- public val userStyle: Map<String, String>,
- public val previewComplicationData: Map<Int, ComplicationData>,
- @get:JvmName("hasCommitChanges")
- public val commitChanges: Boolean
-)
+ public val watchFaceId: WatchFaceId,
+ public val userStyle: UserStyleData,
+ public val previewComplicationsData: Map<Int, ComplicationData>,
+ @get:JvmName("shouldCommitChanges")
+ public val shouldCommitChanges: Boolean
+) {
+ override fun toString(): String =
+ "{watchFaceId: ${watchFaceId.id}, userStyle: $userStyle" +
+ ", previewComplicationsData: [" +
+ previewComplicationsData.map { "${it.key} -> ${it.value}" }.joinToString() +
+ "], shouldCommitChanges: $shouldCommitChanges}"
+}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun EditorStateWireFormat.asApiEditorState(): EditorState {
return EditorState(
- watchFaceInstanceId ?: "",
- userStyle.mUserStyle,
+ WatchFaceId(watchFaceInstanceId ?: ""),
+ UserStyleData(userStyle.mUserStyle),
previewComplicationData.associateBy(
{ it.id },
- { it.complicationData.asApiComplicationData() }
+ { it.complicationData.toApiComplicationData() }
),
commitChanges
)
-}
\ No newline at end of file
+}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index 4ea6283..3234b51 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -17,18 +17,23 @@
package androidx.wear.watchface.client
import android.graphics.Bitmap
-import android.os.IBinder
+import android.os.Bundle
import android.support.wearable.watchface.SharedMemoryImage
+import androidx.annotation.AnyThread
import androidx.annotation.RequiresApi
import androidx.wear.complications.data.ComplicationData
import androidx.wear.utility.TraceEvent
+import androidx.wear.watchface.Complication
+import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.control.IHeadlessWatchFace
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import java.util.concurrent.Executor
/**
* Controls a stateless remote headless watch face. This is mostly intended for use by watch face
@@ -38,6 +43,14 @@
* Note clients should call [close] when finished.
*/
public interface HeadlessWatchFaceClient : AutoCloseable {
+ public companion object {
+ internal const val BINDER_KEY = "HeadlessWatchFaceClient"
+
+ @JvmStatic
+ public fun createFromBundle(bundle: Bundle): HeadlessWatchFaceClient =
+ HeadlessWatchFaceClientImpl(bundle.getBinder(BINDER_KEY) as IHeadlessWatchFace)
+ }
+
/** The UTC reference preview time for this watch face in milliseconds since the epoch. */
public val previewReferenceTimeMillis: Long
@@ -45,21 +58,16 @@
public val userStyleSchema: UserStyleSchema
/**
- * Map of complication ids to [ComplicationState] for each complication slot. Note this can
- * change, typically in response to styling.
+ * Map of complication ids to [ComplicationState] for each [Complication] registered with the
+ * watch face's [ComplicationsManager]. The ComplicationState is based on the initial state of
+ * each Complication plus any overrides from the default style's
+ * [ComplicationsUserStyleSetting]. Because the style can't change, ComplicationState is
+ * immutable for a headless watch face.
*/
- public val complicationState: Map<Int, ComplicationState>
-
- public companion object {
- /** Constructs a [HeadlessWatchFaceClient] from an [IBinder]. */
- @JvmStatic
- public fun createFromBinder(binder: IBinder): HeadlessWatchFaceClient =
- HeadlessWatchFaceClientImpl(binder)
- }
+ public val complicationsState: Map<Int, ComplicationState>
/**
- * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
- * given settings.
+ * Renders the watchface to a shared memory backed [Bitmap] with the given settings.
*
* @param renderParameters The [RenderParameters] to draw with.
* @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
@@ -70,7 +78,7 @@
* given settings.
*/
@RequiresApi(27)
- public fun takeWatchFaceScreenshot(
+ public fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
userStyle: UserStyle?,
@@ -78,8 +86,7 @@
): Bitmap
/**
- * Requests a shared memory backed [Bitmap] containing a screenshot of the complication with the
- * given settings.
+ * Renders the complication to a shared memory backed [Bitmap] with the given settings.
*
* @param complicationId The id of the complication to render
* @param renderParameters The [RenderParameters] to draw with
@@ -90,7 +97,7 @@
* given settings, or `null` if [complicationId] is unrecognized.
*/
@RequiresApi(27)
- public fun takeComplicationScreenshot(
+ public fun renderComplicationToBitmap(
complicationId: Int,
renderParameters: RenderParameters,
calendarTimeMillis: Long,
@@ -98,15 +105,59 @@
userStyle: UserStyle?,
): Bitmap?
- /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
- public fun asBinder(): IBinder
+ /** Callback that observes when the client disconnects. */
+ public interface ClientDisconnectListener {
+ /**
+ * The client disconnected, typically due to the server side crashing. Note this is not
+ * called in response to [close] being called on [HeadlessWatchFaceClient].
+ */
+ public fun onClientDisconnected()
+ }
+
+ /** Registers a [ClientDisconnectListener]. */
+ @AnyThread
+ public fun addClientDisconnectListener(listener: ClientDisconnectListener, executor: Executor)
+
+ /**
+ * Removes a [ClientDisconnectListener] previously registered by [addClientDisconnectListener].
+ */
+ @AnyThread
+ public fun removeClientDisconnectListener(listener: ClientDisconnectListener)
+
+ /** Returns true if the connection to the server side is alive. */
+ @AnyThread
+ public fun isConnectionAlive(): Boolean
+
+ /** Stores the underlying connection in a [Bundle]. */
+ public fun toBundle(): Bundle
}
internal class HeadlessWatchFaceClientImpl internal constructor(
private val iHeadlessWatchFace: IHeadlessWatchFace
) : HeadlessWatchFaceClient {
- constructor(binder: IBinder) : this(IHeadlessWatchFace.Stub.asInterface(binder))
+ private val lock = Any()
+ private val listeners = HashMap<HeadlessWatchFaceClient.ClientDisconnectListener, Executor>()
+
+ init {
+ iHeadlessWatchFace.asBinder().linkToDeath(
+ {
+ var listenerCopy:
+ HashMap<HeadlessWatchFaceClient.ClientDisconnectListener, Executor>
+
+ synchronized(lock) {
+ listenerCopy = HashMap(listeners)
+ }
+
+ for ((listener, executor) in listenerCopy) {
+ executor.execute {
+ listener.onClientDisconnected()
+ }
+ }
+ },
+ 0
+ )
+ }
override val previewReferenceTimeMillis: Long
get() = iHeadlessWatchFace.previewReferenceTimeMillis
@@ -114,22 +165,22 @@
override val userStyleSchema: UserStyleSchema
get() = UserStyleSchema(iHeadlessWatchFace.userStyleSchema)
- override val complicationState: Map<Int, ComplicationState>
+ override val complicationsState: Map<Int, ComplicationState>
get() = iHeadlessWatchFace.complicationState.associateBy(
{ it.id },
{ ComplicationState(it.complicationState) }
)
@RequiresApi(27)
- override fun takeWatchFaceScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
userStyle: UserStyle?,
idToComplicationData: Map<Int, ComplicationData>?
- ): Bitmap = TraceEvent("HeadlessWatchFaceClientImpl.takeWatchFaceScreenshot").use {
+ ): Bitmap = TraceEvent("HeadlessWatchFaceClientImpl.renderWatchFaceToBitmap").use {
SharedMemoryImage.ashmemReadImageBundle(
- iHeadlessWatchFace.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ iHeadlessWatchFace.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
renderParameters.toWireFormat(),
calendarTimeMillis,
userStyle?.toWireFormat(),
@@ -145,15 +196,15 @@
}
@RequiresApi(27)
- override fun takeComplicationScreenshot(
+ override fun renderComplicationToBitmap(
complicationId: Int,
renderParameters: RenderParameters,
calendarTimeMillis: Long,
complicationData: ComplicationData,
userStyle: UserStyle?,
- ): Bitmap? = TraceEvent("HeadlessWatchFaceClientImpl.takeComplicationScreenshot").use {
- iHeadlessWatchFace.takeComplicationScreenshot(
- ComplicationScreenshotParams(
+ ): Bitmap? = TraceEvent("HeadlessWatchFaceClientImpl.renderComplicationToBitmap").use {
+ iHeadlessWatchFace.renderComplicationToBitmap(
+ ComplicationRenderParams(
complicationId,
renderParameters.toWireFormat(),
calendarTimeMillis,
@@ -165,9 +216,33 @@
}
}
+ override fun addClientDisconnectListener(
+ listener: HeadlessWatchFaceClient.ClientDisconnectListener,
+ executor: Executor
+ ) {
+ synchronized(lock) {
+ require(!listeners.contains(listener)) {
+ "Don't call addClientDisconnectListener multiple times for the same listener"
+ }
+ listeners.put(listener, executor)
+ }
+ }
+
+ override fun removeClientDisconnectListener(
+ listener: HeadlessWatchFaceClient.ClientDisconnectListener
+ ) {
+ synchronized(lock) {
+ listeners.remove(listener)
+ }
+ }
+
+ override fun isConnectionAlive() = iHeadlessWatchFace.asBinder().isBinderAlive
+
+ override fun toBundle() = Bundle().apply {
+ this.putBinder(HeadlessWatchFaceClient.BINDER_KEY, iHeadlessWatchFace.asBinder())
+ }
+
override fun close() = TraceEvent("HeadlessWatchFaceClientImpl.close").use {
iHeadlessWatchFace.release()
}
-
- override fun asBinder(): IBinder = iHeadlessWatchFace.asBinder()
}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
new file mode 100644
index 0000000..decd2e9
--- /dev/null
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
@@ -0,0 +1,389 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.client
+
+import android.app.PendingIntent
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.support.wearable.watchface.SharedMemoryImage
+import androidx.annotation.AnyThread
+import androidx.annotation.Px
+import androidx.annotation.RequiresApi
+import androidx.wear.complications.data.ComplicationData
+import androidx.wear.complications.data.ComplicationText
+import androidx.wear.complications.data.toApiComplicationText
+import androidx.wear.utility.TraceEvent
+import androidx.wear.watchface.Complication
+import androidx.wear.watchface.ComplicationsManager
+import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.TapType
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
+import androidx.wear.watchface.data.ComplicationBoundsType
+import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
+import androidx.wear.watchface.data.WatchUiState
+import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
+import java.util.Objects
+import java.util.concurrent.Executor
+
+/**
+ * Controls a stateful remote interactive watch face. Typically this will be used for the current
+ * active watch face.
+ *
+ * Note clients should call [close] when finished.
+ */
+public interface InteractiveWatchFaceClient : AutoCloseable {
+ /**
+ * Sends new ComplicationData to the watch face. Note this doesn't have to be a full update,
+ * it's possible to update just one complication at a time, but doing so may result in a less
+ * visually clean transition.
+ */
+ public fun updateComplicationData(idToComplicationData: Map<Int, ComplicationData>)
+
+ /**
+ * Renders the watchface to a shared memory backed [Bitmap] with the given settings.
+ *
+ * @param renderParameters The [RenderParameters] to draw with.
+ * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
+ * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
+ * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
+ * if null then the existing complication data if any is used.
+ * @return A shared memory backed [Bitmap] containing a screenshot of the watch face with the
+ * given settings.
+ */
+ @RequiresApi(27)
+ public fun renderWatchFaceToBitmap(
+ renderParameters: RenderParameters,
+ calendarTimeMillis: Long,
+ userStyle: UserStyle?,
+ idAndComplicationData: Map<Int, ComplicationData>?
+ ): Bitmap
+
+ /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
+ public val previewReferenceTimeMillis: Long
+
+ /**
+ * Renames this instance to [newInstanceId] (must be unique, usually this would be different
+ * from the old ID but that's not a requirement). Sets the current [UserStyle] and clears
+ * any complication data. Setting the new UserStyle may have a side effect of enabling or
+ * disabling complications, which will be visible via [ComplicationState.isEnabled].
+ */
+ public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle)
+
+ /**
+ * Renames this instance to [newInstanceId] (must be unique, usually this would be different
+ * from the old ID but that's not a requirement). Sets the current [UserStyle] represented as a
+ * [UserStyleData> and clears any complication data. Setting the new UserStyle may have a
+ * side effect of enabling or disabling complications, which will be visible via
+ * [ComplicationState.isEnabled].
+ */
+ public fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyleData)
+
+ /** Returns the ID of this watch face instance. */
+ public val instanceId: String
+
+ /** The watch face's [UserStyleSchema]. */
+ public val userStyleSchema: UserStyleSchema
+
+ /**
+ * Map of complication ids to [ComplicationState] for each [Complication] registered with the
+ * watch face's [ComplicationsManager]. The ComplicationState is based on the initial state of
+ * each Complication plus any overrides from a [ComplicationsUserStyleSetting]. As a
+ * consequence ComplicationState may update based on style changes.
+ */
+ public val complicationsState: Map<Int, ComplicationState>
+
+ /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
+ @SuppressWarnings("AutoBoxing")
+ public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
+ complicationsState.asSequence().firstOrNull {
+ it.value.isEnabled && when (it.value.boundsType) {
+ ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
+ ComplicationBoundsType.BACKGROUND -> false
+ ComplicationBoundsType.EDGE -> false
+ else -> false
+ }
+ }?.key
+
+ /**
+ * Requests that [ComplicationsManager.displayPressedAnimation] is called for [complicationId].
+ */
+ public fun displayPressedAnimation(complicationId: Int)
+
+ public companion object {
+ /** Indicates a "down" touch event on the watch face. */
+ public const val TAP_TYPE_DOWN: Int = IInteractiveWatchFace.TAP_TYPE_DOWN
+
+ /**
+ * Indicates that a previous [TAP_TYPE_DOWN] event has been canceled. This generally happens
+ * when the watch face is touched but then a move or long press occurs.
+ */
+ public const val TAP_TYPE_CANCEL: Int = IInteractiveWatchFace.TAP_TYPE_CANCEL
+
+ /**
+ * Indicates that an "up" event on the watch face has occurred that has not been consumed by
+ * another activity. A [TAP_TYPE_DOWN] always occur first. This event will not occur if a
+ * [TAP_TYPE_CANCEL] is sent.
+ */
+ public const val TAP_TYPE_UP: Int = IInteractiveWatchFace.TAP_TYPE_UP
+ }
+
+ /**
+ * Sends a tap event to the watch face for processing.
+ */
+ public fun sendTouchEvent(@Px xPosition: Int, @Px yPosition: Int, @TapType tapType: Int)
+
+ /**
+ * Describes regions of the watch face for use by a screen reader.
+ *
+ * @param text [ComplicationText] associated with the region, to be read by the screen reader.
+ * @param bounds [Rect] describing the area of the feature on screen.
+ * @param tapAction [PendingIntent] to be used if the screen reader's user triggers a tap
+ * action.
+ */
+ public class ContentDescriptionLabel(
+ private val text: ComplicationText,
+ public val bounds: Rect,
+ public val tapAction: PendingIntent?
+ ) {
+ /**
+ * Returns the text that should be displayed for the given timestamp.
+ *
+ * @param resources [Resources] from the current [android.content.Context]
+ * @param dateTimeMillis milliseconds since epoch, e.g. from [System.currentTimeMillis]
+ */
+ public fun getTextAt(resources: Resources, dateTimeMillis: Long): CharSequence =
+ text.getTextAt(resources, dateTimeMillis)
+
+ override fun equals(other: Any?): Boolean =
+ other is ContentDescriptionLabel &&
+ text == other.text &&
+ bounds == other.bounds &&
+ tapAction == other.tapAction
+
+ override fun hashCode(): Int {
+ return Objects.hash(
+ text,
+ bounds,
+ tapAction
+ )
+ }
+ }
+
+ /**
+ * Returns the [ContentDescriptionLabel]s describing the watch face, for the use by screen
+ * readers.
+ */
+ public val contentDescriptionLabels: List<ContentDescriptionLabel>
+
+ /** Updates the watch faces [WatchUiState]. */
+ public fun setWatchUiState(watchUiState: androidx.wear.watchface.client.WatchUiState)
+
+ /** Triggers watch face rendering into the surface when in ambient mode. */
+ public fun performAmbientTick()
+
+ /** Callback that observes when the client disconnects. */
+ public interface ClientDisconnectListener {
+ /**
+ * The client disconnected, typically due to the server side crashing. Note this is not
+ * called in response to [close] being called on [InteractiveWatchFaceClient].
+ */
+ public fun onClientDisconnected()
+ }
+
+ /** Registers a [ClientDisconnectListener]. */
+ @AnyThread
+ public fun addClientDisconnectListener(listener: ClientDisconnectListener, executor: Executor)
+
+ /**
+ * Removes a [ClientDisconnectListener] previously registered by [addClientDisconnectListener].
+ */
+ @AnyThread
+ public fun removeClientDisconnectListener(listener: ClientDisconnectListener)
+
+ /** Returns true if the connection to the server side is alive. */
+ @AnyThread
+ public fun isConnectionAlive(): Boolean
+}
+
+/** Controls a stateful remote interactive watch face. */
+internal class InteractiveWatchFaceClientImpl internal constructor(
+ private val iInteractiveWatchFace: IInteractiveWatchFace
+) : InteractiveWatchFaceClient {
+
+ private val lock = Any()
+ private val listeners = HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>()
+
+ init {
+ iInteractiveWatchFace.asBinder().linkToDeath(
+ {
+ var listenerCopy:
+ HashMap<InteractiveWatchFaceClient.ClientDisconnectListener, Executor>
+
+ synchronized(lock) {
+ listenerCopy = HashMap(listeners)
+ }
+
+ for ((listener, executor) in listenerCopy) {
+ executor.execute {
+ listener.onClientDisconnected()
+ }
+ }
+ },
+ 0
+ )
+ }
+
+ override fun updateComplicationData(
+ idToComplicationData: Map<Int, ComplicationData>
+ ) = TraceEvent("InteractiveWatchFaceClientImpl.updateComplicationData").use {
+ iInteractiveWatchFace.updateComplicationData(
+ idToComplicationData.map {
+ IdAndComplicationDataWireFormat(it.key, it.value.asWireComplicationData())
+ }
+ )
+ }
+
+ @RequiresApi(27)
+ override fun renderWatchFaceToBitmap(
+ renderParameters: RenderParameters,
+ calendarTimeMillis: Long,
+ userStyle: UserStyle?,
+ idAndComplicationData: Map<Int, ComplicationData>?
+ ): Bitmap = TraceEvent("InteractiveWatchFaceClientImpl.renderWatchFaceToBitmap").use {
+ SharedMemoryImage.ashmemReadImageBundle(
+ iInteractiveWatchFace.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
+ renderParameters.toWireFormat(),
+ calendarTimeMillis,
+ userStyle?.toWireFormat(),
+ idAndComplicationData?.map {
+ IdAndComplicationDataWireFormat(
+ it.key,
+ it.value.asWireComplicationData()
+ )
+ }
+ )
+ )
+ )
+ }
+
+ override val previewReferenceTimeMillis: Long
+ get() = iInteractiveWatchFace.previewReferenceTimeMillis
+
+ override fun updateWatchFaceInstance(newInstanceId: String, userStyle: UserStyle) = TraceEvent(
+ "InteractiveWatchFaceClientImpl.updateInstance"
+ ).use {
+ iInteractiveWatchFace.updateWatchfaceInstance(newInstanceId, userStyle.toWireFormat())
+ }
+
+ override fun updateWatchFaceInstance(
+ newInstanceId: String,
+ userStyle: UserStyleData
+ ) = TraceEvent(
+ "InteractiveWatchFaceClientImpl.updateInstance"
+ ).use {
+ iInteractiveWatchFace.updateWatchfaceInstance(
+ newInstanceId,
+ userStyle.toWireFormat()
+ )
+ }
+
+ override val instanceId: String
+ get() = iInteractiveWatchFace.instanceId
+
+ override val userStyleSchema: UserStyleSchema
+ get() = UserStyleSchema(iInteractiveWatchFace.userStyleSchema)
+
+ override val complicationsState: Map<Int, ComplicationState>
+ get() = iInteractiveWatchFace.complicationDetails.associateBy(
+ { it.id },
+ { ComplicationState(it.complicationState) }
+ )
+
+ override fun close() = TraceEvent("InteractiveWatchFaceClientImpl.close").use {
+ iInteractiveWatchFace.release()
+ }
+
+ override fun displayPressedAnimation(complicationId: Int) = TraceEvent(
+ "InteractiveWatchFaceClientImpl.bringAttentionToComplication"
+ ).use {
+ iInteractiveWatchFace.bringAttentionToComplication(complicationId)
+ }
+
+ override fun sendTouchEvent(
+ xPosition: Int,
+ yPosition: Int,
+ @TapType tapType: Int
+ ) = TraceEvent("InteractiveWatchFaceClientImpl.sendTouchEvent").use {
+ iInteractiveWatchFace.sendTouchEvent(xPosition, yPosition, tapType)
+ }
+
+ override val contentDescriptionLabels: List<InteractiveWatchFaceClient.ContentDescriptionLabel>
+ get() = iInteractiveWatchFace.contentDescriptionLabels.map {
+ InteractiveWatchFaceClient.ContentDescriptionLabel(
+ it.text.toApiComplicationText(),
+ it.bounds,
+ it.tapAction
+ )
+ }
+
+ override fun setWatchUiState(
+ watchUiState: androidx.wear.watchface.client.WatchUiState
+ ) = TraceEvent(
+ "InteractiveWatchFaceClientImpl.setSystemState"
+ ).use {
+ iInteractiveWatchFace.setWatchUiState(
+ WatchUiState(
+ watchUiState.inAmbientMode,
+ watchUiState.interruptionFilter
+ )
+ )
+ }
+
+ override fun performAmbientTick() = TraceEvent(
+ "InteractiveWatchFaceClientImpl.performAmbientTick"
+ ).use {
+ iInteractiveWatchFace.ambientTickUpdate()
+ }
+
+ override fun addClientDisconnectListener(
+ listener: InteractiveWatchFaceClient.ClientDisconnectListener,
+ executor: Executor
+ ) {
+ synchronized(lock) {
+ require(!listeners.contains(listener)) {
+ "Don't call addClientDisconnectListener multiple times for the same listener"
+ }
+ listeners.put(listener, executor)
+ }
+ }
+
+ override fun removeClientDisconnectListener(
+ listener: InteractiveWatchFaceClient.ClientDisconnectListener
+ ) {
+ synchronized(lock) {
+ listeners.remove(listener)
+ }
+ }
+
+ override fun isConnectionAlive() = iInteractiveWatchFace.asBinder().isBinderAlive
+}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
deleted file mode 100644
index 334dcf4..0000000
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.client
-
-import android.app.PendingIntent
-import android.content.res.Resources
-import android.graphics.Bitmap
-import android.graphics.Rect
-import android.os.IBinder
-import android.support.wearable.complications.TimeDependentText
-import android.support.wearable.watchface.SharedMemoryImage
-import androidx.annotation.IntDef
-import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
-import androidx.wear.complications.data.ComplicationData
-import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.RenderParameters
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
-import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.style.UserStyle
-import java.util.Objects
-
-/**
- * The type of tap event passed to the watch face.
- * @hide
- */
-@IntDef(
- InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH,
- InteractiveWatchFaceSysUiClient.TAP_TYPE_TOUCH_CANCEL,
- InteractiveWatchFaceSysUiClient.TAP_TYPE_TAP
-)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public annotation class TapType
-
-/**
- * Controls a stateful remote interactive watch face with an interface tailored for SysUI the
- * WearOS launcher app. Typically this will be used for the current active watch face.
- *
- * Note clients should call [close] when finished.
- */
-public interface InteractiveWatchFaceSysUiClient : AutoCloseable {
-
- public companion object {
- /** Indicates a "down" touch event on the watch face. */
- public const val TAP_TYPE_TOUCH: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH
-
- /**
- * Indicates that a previous TAP_TYPE_TOUCH event has been canceled. This generally happens
- * when the watch face is touched but then a move or long press occurs.
- */
- public const val TAP_TYPE_TOUCH_CANCEL: Int =
- IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL
-
- /**
- * Indicates that an "up" event on the watch face has occurred that has not been consumed by
- * another activity. A TAP_TYPE_TOUCH always occur first. This event will not occur if a
- * TAP_TYPE_TOUCH_CANCEL is sent.
- */
- public const val TAP_TYPE_TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
-
- /**
- * Constructs an [InteractiveWatchFaceSysUiClient] from the [IBinder] returned by
- * [asBinder].
- */
- @JvmStatic
- public fun createFromBinder(binder: IBinder): InteractiveWatchFaceSysUiClient =
- InteractiveWatchFaceSysUiClientImpl(binder)
- }
-
- /**
- * Sends a tap event to the watch face for processing.
- */
- public fun sendTouchEvent(xPosition: Int, yPosition: Int, @TapType tapType: Int)
-
- /**
- * Describes regions of the watch face for use by a screen reader.
- *
- * @param text Text associated with the region, to be read by the screen reader.
- * @param bounds [Rect] describing the area of the feature on screen.
- * @param tapAction [PendingIntent] to be used if the screen reader's user triggers a tap
- * action.
- */
- public class ContentDescriptionLabel(
- private val text: TimeDependentText,
- public val bounds: Rect,
- public val tapAction: PendingIntent?
- ) {
- /**
- * Returns the text that should be displayed for the given timestamp.
- *
- * @param resources [Resources] from the current [android.content.Context]
- * @param dateTimeMillis milliseconds since epoch, e.g. from [System.currentTimeMillis]
- */
- public fun getTextAt(resources: Resources, dateTimeMillis: Long): CharSequence =
- text.getTextAt(resources, dateTimeMillis)
-
- override fun equals(other: Any?): Boolean =
- other is ContentDescriptionLabel &&
- text == other.text &&
- bounds == other.bounds &&
- tapAction == other.tapAction
-
- override fun hashCode(): Int {
- return Objects.hash(
- text,
- bounds,
- tapAction
- )
- }
- }
-
- /**
- * Returns the [ContentDescriptionLabel]s describing the watch face, for the use by screen
- * readers.
- */
- public val contentDescriptionLabels: List<ContentDescriptionLabel>
-
- /**
- * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
- * given settings.
- *
- * @param renderParameters The [RenderParameters] to draw with.
- * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
- * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
- * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
- * if null then the existing complication data if any is used.
- * @return A shared memory backed [Bitmap] containing a screenshot of the watch face with the
- * given settings.
- */
- @RequiresApi(27)
- public fun takeWatchFaceScreenshot(
- renderParameters: RenderParameters,
- calendarTimeMillis: Long,
- userStyle: UserStyle?,
- idAndComplicationData: Map<Int, ComplicationData>?
- ): Bitmap
-
- /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
- public val previewReferenceTimeMillis: Long
-
- /** Updates the watch faces [SystemState]. */
- public fun setSystemState(systemState: SystemState)
-
- /** Returns the ID of this watch face instance. */
- public val instanceId: String
-
- /** Triggers watch face rendering into the surface when in ambient mode. */
- public fun performAmbientTick()
-
- /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
- public fun asBinder(): IBinder
-}
-
-internal class InteractiveWatchFaceSysUiClientImpl internal constructor(
- private val iInteractiveWatchFaceSysUI: IInteractiveWatchFaceSysUI
-) : InteractiveWatchFaceSysUiClient {
-
- constructor(binder: IBinder) : this(IInteractiveWatchFaceSysUI.Stub.asInterface(binder))
-
- override fun sendTouchEvent(
- xPosition: Int,
- yPosition: Int,
- @TapType tapType: Int
- ) = TraceEvent("InteractiveWatchFaceSysUiClientImpl.sendTouchEvent").use {
- iInteractiveWatchFaceSysUI.sendTouchEvent(xPosition, yPosition, tapType)
- }
-
- override val contentDescriptionLabels:
- List<InteractiveWatchFaceSysUiClient.ContentDescriptionLabel>
- get() = iInteractiveWatchFaceSysUI.contentDescriptionLabels.map {
- InteractiveWatchFaceSysUiClient.ContentDescriptionLabel(
- it.text,
- it.bounds,
- it.tapAction
- )
- }
-
- @RequiresApi(27)
- override fun takeWatchFaceScreenshot(
- renderParameters: RenderParameters,
- calendarTimeMillis: Long,
- userStyle: UserStyle?,
- idAndComplicationData: Map<Int, ComplicationData>?
- ): Bitmap = TraceEvent("InteractiveWatchFaceSysUiClientImpl.takeWatchFaceScreenshot").use {
- SharedMemoryImage.ashmemReadImageBundle(
- iInteractiveWatchFaceSysUI.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
- renderParameters.toWireFormat(),
- calendarTimeMillis,
- userStyle?.toWireFormat(),
- idAndComplicationData?.map {
- IdAndComplicationDataWireFormat(
- it.key,
- it.value.asWireComplicationData()
- )
- }
- )
- )
- )
- }
-
- override val previewReferenceTimeMillis: Long
- get() = iInteractiveWatchFaceSysUI.previewReferenceTimeMillis
-
- override fun setSystemState(systemState: SystemState) = TraceEvent(
- "InteractiveWatchFaceSysUiClientImpl.setSystemState"
- ).use {
- iInteractiveWatchFaceSysUI.setSystemState(
- androidx.wear.watchface.data.SystemState(
- systemState.inAmbientMode,
- systemState.interruptionFilter
- )
- )
- }
-
- override val instanceId: String
- get() = iInteractiveWatchFaceSysUI.instanceId
-
- override fun performAmbientTick() = TraceEvent(
- "InteractiveWatchFaceSysUiClientImpl.performAmbientTick"
- ).use {
- iInteractiveWatchFaceSysUI.ambientTickUpdate()
- }
-
- override fun close() = TraceEvent("InteractiveWatchFaceSysUiClientImpl.close").use {
- iInteractiveWatchFaceSysUI.release()
- }
-
- override fun asBinder(): IBinder = iInteractiveWatchFaceSysUI.asBinder()
-}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
deleted file mode 100644
index e60a906..0000000
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.client
-
-import android.graphics.Bitmap
-import android.os.IBinder
-import android.support.wearable.watchface.SharedMemoryImage
-import androidx.annotation.Px
-import androidx.annotation.RequiresApi
-import androidx.wear.complications.data.ComplicationData
-import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.RenderParameters
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
-import androidx.wear.watchface.data.ComplicationBoundsType
-import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleSchema
-import androidx.wear.watchface.style.data.UserStyleWireFormat
-
-/**
- * Controls a stateful remote interactive watch face with an interface tailored for WCS the
- * WearOS system server responsible for watch face management. Typically this will be used for
- * the current active watch face.
- *
- * Note clients should call [close] when finished.
- */
-public interface InteractiveWatchFaceWcsClient : AutoCloseable {
-
- public companion object {
- /**
- * Constructs an [InteractiveWatchFaceWcsClient] from the [IBinder] returned by [asBinder].
- */
- @JvmStatic
- public fun createFromBinder(binder: IBinder): InteractiveWatchFaceWcsClient =
- InteractiveWatchFaceWcsClientImpl(binder)
- }
-
- /**
- * Sends new ComplicationData to the watch face. Note this doesn't have to be a full update,
- * it's possible to update just one complication at a time, but doing so may result in a less
- * visually clean transition.
- */
- public fun updateComplicationData(idToComplicationData: Map<Int, ComplicationData>)
-
- /**
- * Requests a shared memory backed [Bitmap] containing a screenshot of the watch face with the
- * given settings.
- *
- * @param renderParameters The [RenderParameters] to draw with.
- * @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with.
- * @param userStyle Optional [UserStyle] to render with, if null the current style is used.
- * @param idAndComplicationData Map of complication ids to [ComplicationData] to render with, or
- * if null then the existing complication data if any is used.
- * @return A shared memory backed [Bitmap] containing a screenshot of the watch face with the
- * given settings.
- */
- @RequiresApi(27)
- public fun takeWatchFaceScreenshot(
- renderParameters: RenderParameters,
- calendarTimeMillis: Long,
- userStyle: UserStyle?,
- idAndComplicationData: Map<Int, ComplicationData>?
- ): Bitmap
-
- /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
- public val previewReferenceTimeMillis: Long
-
- /**
- * Renames this instance to [newInstanceId] (must be unique, usually this would be different
- * from the old ID but that's not a requirement). Sets the current [UserStyle] and clears
- * any complication data. Setting the new UserStyle may have a side effect of enabling or
- * disabling complications, which will be visible via [ComplicationState.isEnabled].
- */
- public fun updateInstance(newInstanceId: String, userStyle: UserStyle)
-
- /**
- * Renames this instance to [newInstanceId] (must be unique, usually this would be different
- * from the old ID but that's not a requirement). Sets the current [UserStyle] represented as a
- * Map<String, String> and clears any complication data. Setting the new UserStyle may have
- * a side effect of enabling or disabling complications, which will be visible via
- * [ComplicationState.isEnabled].
- */
- public fun updateInstance(newInstanceId: String, userStyle: Map<String, String>)
-
- /** Returns the ID of this watch face instance. */
- public val instanceId: String
-
- /** The watch face's [UserStyleSchema]. */
- public val userStyleSchema: UserStyleSchema
-
- /**
- * Map of complication ids to [ComplicationState] for each complication slot. Note
- * this can change, typically in response to styling.
- */
- public val complicationState: Map<Int, ComplicationState>
-
- /** Returns the associated [IBinder]. Allows this interface to be passed over AIDL. */
- public fun asBinder(): IBinder
-
- /** Returns the ID of the complication at the given coordinates or `null` if there isn't one.*/
- @SuppressWarnings("AutoBoxing")
- public fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? =
- complicationState.asSequence().firstOrNull {
- it.value.isEnabled && when (it.value.boundsType) {
- ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
- ComplicationBoundsType.BACKGROUND -> false
- ComplicationBoundsType.EDGE -> false
- else -> false
- }
- }?.key
-
- /**
- * Requests the specified complication is highlighted for a short period to bring attention to
- * it.
- */
- public fun bringAttentionToComplication(complicationId: Int)
-}
-
-/** Controls a stateful remote interactive watch face with an interface tailored for WCS. */
-internal class InteractiveWatchFaceWcsClientImpl internal constructor(
- private val iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
-) : InteractiveWatchFaceWcsClient {
-
- constructor(binder: IBinder) : this(IInteractiveWatchFaceWCS.Stub.asInterface(binder))
-
- override fun updateComplicationData(
- idToComplicationData: Map<Int, ComplicationData>
- ) = TraceEvent("InteractiveWatchFaceWcsClientImpl.updateComplicationData").use {
- iInteractiveWatchFaceWcs.updateComplicationData(
- idToComplicationData.map {
- IdAndComplicationDataWireFormat(it.key, it.value.asWireComplicationData())
- }
- )
- }
-
- @RequiresApi(27)
- override fun takeWatchFaceScreenshot(
- renderParameters: RenderParameters,
- calendarTimeMillis: Long,
- userStyle: UserStyle?,
- idAndComplicationData: Map<Int, ComplicationData>?
- ): Bitmap = TraceEvent("InteractiveWatchFaceWcsClientImpl.takeWatchFaceScreenshot").use {
- SharedMemoryImage.ashmemReadImageBundle(
- iInteractiveWatchFaceWcs.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
- renderParameters.toWireFormat(),
- calendarTimeMillis,
- userStyle?.toWireFormat(),
- idAndComplicationData?.map {
- IdAndComplicationDataWireFormat(
- it.key,
- it.value.asWireComplicationData()
- )
- }
- )
- )
- )
- }
-
- override val previewReferenceTimeMillis: Long
- get() = iInteractiveWatchFaceWcs.previewReferenceTimeMillis
-
- override fun updateInstance(newInstanceId: String, userStyle: UserStyle) = TraceEvent(
- "InteractiveWatchFaceWcsClientImpl.updateInstance"
- ).use {
- iInteractiveWatchFaceWcs.updateInstance(newInstanceId, userStyle.toWireFormat())
- }
-
- override fun updateInstance(newInstanceId: String, userStyle: Map<String, String>) = TraceEvent(
- "InteractiveWatchFaceWcsClientImpl.updateInstance"
- ).use {
- iInteractiveWatchFaceWcs.updateInstance(newInstanceId, UserStyleWireFormat(userStyle))
- }
-
- override val instanceId: String
- get() = iInteractiveWatchFaceWcs.instanceId
-
- override val userStyleSchema: UserStyleSchema
- get() = UserStyleSchema(iInteractiveWatchFaceWcs.userStyleSchema)
-
- override val complicationState: Map<Int, ComplicationState>
- get() = iInteractiveWatchFaceWcs.complicationDetails.associateBy(
- { it.id },
- { ComplicationState(it.complicationState) }
- )
-
- override fun close() = TraceEvent("InteractiveWatchFaceWcsClientImpl.close").use {
- iInteractiveWatchFaceWcs.release()
- }
-
- override fun asBinder(): IBinder = iInteractiveWatchFaceWcs.asBinder()
-
- override fun bringAttentionToComplication(complicationId: Int) = TraceEvent(
- "InteractiveWatchFaceWcsClientImpl.bringAttentionToComplication"
- ).use {
- iInteractiveWatchFaceWcs.bringAttentionToComplication(complicationId)
- }
-}
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index c06d5ac..652bfb12 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -22,20 +22,25 @@
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
+import androidx.annotation.Px
import androidx.wear.complications.data.ComplicationData
import androidx.wear.utility.AsyncTraceEvent
import androidx.wear.utility.TraceEvent
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.data.UserStyleWireFormat
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
/**
* Connects to a watch face's WatchFaceControlService which allows the user to control the watch
@@ -103,22 +108,22 @@
public class ServiceStartFailureException : Exception()
/**
- * Returns the [InteractiveWatchFaceSysUiClient] for the given instance id, or null if no such
+ * Returns the [InteractiveWatchFaceClient] for the given instance id, or null if no such
* instance exists.
*
- * When finished call [InteractiveWatchFaceSysUiClient.close] to release resources.
+ * When finished call [InteractiveWatchFaceClient.close] to release resources.
*
* @param instanceId The name of the interactive watch face instance to retrieve
- * @return The [InteractiveWatchFaceSysUiClient] or `null` if [instanceId] is unrecognized,
+ * @return The [InteractiveWatchFaceClient] or `null` if [instanceId] is unrecognized,
* or [ServiceNotBoundException] if the WatchFaceControlService is not bound.
*/
- public fun getInteractiveWatchFaceSysUiClientInstance(
+ public fun getInteractiveWatchFaceClientInstance(
instanceId: String
- ): InteractiveWatchFaceSysUiClient?
+ ): InteractiveWatchFaceClient?
/**
* Creates a [HeadlessWatchFaceClient] with the specified [DeviceConfig]. Screenshots made with
- * [HeadlessWatchFaceClient.takeWatchFaceScreenshot] will be `surfaceWidth` x `surfaceHeight` in
+ * [HeadlessWatchFaceClient.renderWatchFaceToBitmap] will be `surfaceWidth` x `surfaceHeight` in
* size.
*
* When finished call [HeadlessWatchFaceClient.close] to release resources.
@@ -134,33 +139,33 @@
public fun createHeadlessWatchFaceClient(
watchFaceName: ComponentName,
deviceConfig: DeviceConfig,
- surfaceWidth: Int,
- surfaceHeight: Int
+ @Px surfaceWidth: Int,
+ @Px surfaceHeight: Int
): HeadlessWatchFaceClient?
/**
- * Requests either an existing [InteractiveWatchFaceWcsClient] with the specified [id] or
- * schedules creation of an [InteractiveWatchFaceWcsClient] for the next time the
+ * Requests either an existing [InteractiveWatchFaceClient] with the specified [id] or
+ * schedules creation of an [InteractiveWatchFaceClient] for the next time the
* WallpaperService creates an engine.
*
- * NOTE that currently only one [InteractiveWatchFaceWcsClient] per process can exist at a time.
+ * NOTE that currently only one [InteractiveWatchFaceClient] per process can exist at a time.
*
- * @param id The ID for the requested [InteractiveWatchFaceWcsClient].
+ * @param id The ID for the requested [InteractiveWatchFaceClient].
* @param deviceConfig The [DeviceConfig] for the wearable.
- * @param systemState The initial [SystemState] for the wearable.
- * @param userStyle The initial style map (see [UserStyle]), or null if the default should be
- * used.
+ * @param watchUiState The initial [WatchUiState] for the wearable.
+ * @param userStyle The initial style map encoded as [UserStyleData] (see [UserStyle]),
+ * or null if the default should be used.
* @param idToComplicationData The initial complication data, or null if unavailable.
- * @return A [Deferred] [InteractiveWatchFaceWcsClient], or a [ServiceStartFailureException] if
- * the watchface dies during startup.
+ * @return The [InteractiveWatchFaceClient], this should be closed when finished.
+ * @throws [ServiceStartFailureException] if the watchface dies during startup.
*/
- public fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ public suspend fun getOrCreateInteractiveWatchFaceClient(
id: String,
deviceConfig: DeviceConfig,
- systemState: SystemState,
- userStyle: Map<String, String>?,
+ watchUiState: androidx.wear.watchface.client.WatchUiState,
+ userStyle: UserStyleData?,
idToComplicationData: Map<Int, ComplicationData>?
- ): Deferred<InteractiveWatchFaceWcsClient>
+ ): InteractiveWatchFaceClient
public fun getEditorServiceClient(): EditorServiceClient
}
@@ -172,10 +177,10 @@
) : WatchFaceControlClient {
private var closed = false
- override fun getInteractiveWatchFaceSysUiClientInstance(
+ override fun getInteractiveWatchFaceClientInstance(
instanceId: String
- ) = service.getInteractiveWatchFaceInstanceSysUI(instanceId)?.let {
- InteractiveWatchFaceSysUiClientImpl(it)
+ ) = service.getInteractiveWatchFaceInstance(instanceId)?.let {
+ InteractiveWatchFaceClientImpl(it)
}
override fun createHeadlessWatchFaceClient(
@@ -204,74 +209,71 @@
}
}
- override fun getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync(
+ override suspend fun getOrCreateInteractiveWatchFaceClient(
id: String,
deviceConfig: DeviceConfig,
- systemState: SystemState,
- userStyle: Map<String, String>?,
+ watchUiState: androidx.wear.watchface.client.WatchUiState,
+ userStyle: UserStyleData?,
idToComplicationData: Map<Int, ComplicationData>?
- ): Deferred<InteractiveWatchFaceWcsClient> {
+ ): InteractiveWatchFaceClient {
requireNotClosed()
val traceEvent = AsyncTraceEvent(
"WatchFaceControlClientImpl" +
- ".getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClientAsync"
+ ".getOrCreateWallpaperServiceBackedInteractiveWatchFaceClientAsync"
)
- val deferredClient = CompletableDeferred<InteractiveWatchFaceWcsClient>()
-
- // [IWatchFaceControlService.getOrCreateInteractiveWatchFaceWCS] has an asynchronous
- // callback and it's possible the watch face might crash during start up so we register
- // a death observer.
- val deathObserver = IBinder.DeathRecipient {
- deferredClient.completeExceptionally(
- WatchFaceControlClient.ServiceStartFailureException()
- )
- }
- val serviceBinder = service.asBinder()
- serviceBinder.linkToDeath(deathObserver, 0)
-
- service.getOrCreateInteractiveWatchFaceWCS(
- WallpaperInteractiveWatchFaceInstanceParams(
- id,
- androidx.wear.watchface.data.DeviceConfig(
- deviceConfig.hasLowBitAmbient,
- deviceConfig.hasBurnInProtection,
- deviceConfig.analogPreviewReferenceTimeMillis,
- deviceConfig.digitalPreviewReferenceTimeMillis
- ),
- androidx.wear.watchface.data.SystemState(
- systemState.inAmbientMode,
- systemState.interruptionFilter
- ),
- UserStyleWireFormat(userStyle ?: emptyMap()),
- idToComplicationData?.map {
- IdAndComplicationDataWireFormat(
- it.key,
- it.value.asWireComplicationData()
- )
- }
- ),
- object : IPendingInteractiveWatchFaceWCS.Stub() {
- override fun getApiVersion() = IPendingInteractiveWatchFaceWCS.API_VERSION
-
- override fun onInteractiveWatchFaceWcsCreated(
- iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
- ) {
- serviceBinder.unlinkToDeath(deathObserver, 0)
- traceEvent.close()
- deferredClient.complete(
- InteractiveWatchFaceWcsClientImpl(iInteractiveWatchFaceWcs)
- )
- }
+ return suspendCoroutine { continuation ->
+ // [IWatchFaceControlService.getOrCreateInteractiveWatchFaceWCS] has an asynchronous
+ // callback and it's possible the watch face might crash during start up so we register
+ // a death observer.
+ val deathObserver = IBinder.DeathRecipient {
+ continuation.resumeWithException(
+ WatchFaceControlClient.ServiceStartFailureException()
+ )
}
- )?.let {
- // There was an existing watchface.onInteractiveWatchFaceWcsCreated
- serviceBinder.unlinkToDeath(deathObserver, 0)
- traceEvent.close()
- deferredClient.complete(InteractiveWatchFaceWcsClientImpl(it))
- }
+ val serviceBinder = service.asBinder()
+ serviceBinder.linkToDeath(deathObserver, 0)
- // Wait for [watchFaceCreatedCallback] or [deathObserver] to fire.
- return deferredClient
+ service.getOrCreateInteractiveWatchFace(
+ WallpaperInteractiveWatchFaceInstanceParams(
+ id,
+ androidx.wear.watchface.data.DeviceConfig(
+ deviceConfig.hasLowBitAmbient,
+ deviceConfig.hasBurnInProtection,
+ deviceConfig.analogPreviewReferenceTimeMillis,
+ deviceConfig.digitalPreviewReferenceTimeMillis
+ ),
+ WatchUiState(
+ watchUiState.inAmbientMode,
+ watchUiState.interruptionFilter
+ ),
+ userStyle?.toWireFormat() ?: UserStyleWireFormat(emptyMap()),
+ idToComplicationData?.map {
+ IdAndComplicationDataWireFormat(
+ it.key,
+ it.value.asWireComplicationData()
+ )
+ }
+ ),
+ object : IPendingInteractiveWatchFace.Stub() {
+ override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
+ ) {
+ serviceBinder.unlinkToDeath(deathObserver, 0)
+ traceEvent.close()
+ continuation.resume(
+ InteractiveWatchFaceClientImpl(iInteractiveWatchFace)
+ )
+ }
+ }
+ )?.let {
+ // There was an existing watchface.onInteractiveWatchFaceCreated
+ serviceBinder.unlinkToDeath(deathObserver, 0)
+ traceEvent.close()
+ continuation.resume(InteractiveWatchFaceClientImpl(it))
+ }
+ }
}
override fun getEditorServiceClient(): EditorServiceClient = TraceEvent(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
similarity index 70%
rename from wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt
rename to wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
index 748c9a6..e9da6de 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/SystemState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchUiState.kt
@@ -35,13 +35,19 @@
public annotation class InterruptionFilter
/**
- * Describes the system state of the watch face.
+ * Describes the system state of the watch face ui.
*
* @param inAmbientMode Whether the device is is ambient mode or not.
- * @param interruptionFilter The current user interruption settings. @see [NotificationManager]
- * for details.
+ * @param interruptionFilter The interruption filter defines which notifications are allowed to
+ * interrupt the user. For watch faces this value is one of:
+ * [NotificationManager.INTERRUPTION_FILTER_ALARMS],
+ * [NotificationManager.INTERRUPTION_FILTER_ALL],
+ * [NotificationManager.INTERRUPTION_FILTER_NONE],
+ * [NotificationManager.INTERRUPTION_FILTER_PRIORITY],
+ * [NotificationManager.INTERRUPTION_FILTER_UNKNOWN]. @see [NotificationManager] for more
+ * details.
*/
-public class SystemState(
+public class WatchUiState(
@get:JvmName("inAmbientMode")
public val inAmbientMode: Boolean,
diff --git a/wear/wear-watchface-complications-rendering/build.gradle b/wear/wear-watchface-complications-rendering/build.gradle
index 3cbc76b..7307faf 100644
--- a/wear/wear-watchface-complications-rendering/build.gradle
+++ b/wear/wear-watchface-complications-rendering/build.gradle
@@ -64,7 +64,7 @@
name = "Android Wear Watchface Complication Rendering"
publish = Publish.SNAPSHOT_AND_RELEASE
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_WATCHFACE
+ mavenVersion = LibraryVersions.WEAR_WATCHFACE_COMPLICATIONS_RENDERING
inceptionYear = "2020"
description = "Support for rendering complications on the watch face"
}
diff --git a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index 3519721..1492085 100644
--- a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -56,7 +56,7 @@
import androidx.wear.watchface.WatchFaceService;
import androidx.wear.watchface.WatchFaceType;
import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
import androidx.wear.watchface.style.UserStyleSchema;
import org.jetbrains.annotations.Nullable;
@@ -720,14 +720,14 @@
protected Object createWatchFace(@NonNull SurfaceHolder surfaceHolder,
@NonNull WatchState watchState,
@NonNull Continuation<? super WatchFace> completion) {
- UserStyleRepository userStyleRepository =
- new UserStyleRepository(new UserStyleSchema(new ArrayList<>()));
+ CurrentUserStyleRepository currentUserStyleRepository =
+ new CurrentUserStyleRepository(new UserStyleSchema(new ArrayList<>()));
return new WatchFace(
WatchFaceType.ANALOG,
- userStyleRepository,
+ currentUserStyleRepository,
new Renderer.CanvasRenderer(
- surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
- 16L) {
+ surfaceHolder, currentUserStyleRepository, watchState,
+ CanvasType.SOFTWARE, 16L) {
@Override
public void render(@NonNull Canvas canvas, @NonNull Rect bounds,
@NonNull Calendar calendar) {
diff --git a/wear/wear-watchface-data/api/restricted_current.txt b/wear/wear-watchface-data/api/restricted_current.txt
index 3415e93..cba8655 100644
--- a/wear/wear-watchface-data/api/restricted_current.txt
+++ b/wear/wear-watchface-data/api/restricted_current.txt
@@ -123,8 +123,8 @@
package androidx.wear.watchface.control.data {
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationScreenshotParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public ComplicationScreenshotParams(int, androidx.wear.watchface.data.RenderParametersWireFormat, long, android.support.wearable.complications.ComplicationData?, androidx.wear.watchface.style.data.UserStyleWireFormat?);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationRenderParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+ ctor public ComplicationRenderParams(int, androidx.wear.watchface.data.RenderParametersWireFormat, long, android.support.wearable.complications.ComplicationData?, androidx.wear.watchface.style.data.UserStyleWireFormat?);
method public int describeContents();
method public long getCalendarTimeMillis();
method public android.support.wearable.complications.ComplicationData? getComplicationData();
@@ -132,7 +132,7 @@
method public androidx.wear.watchface.data.RenderParametersWireFormat getRenderParametersWireFormat();
method public androidx.wear.watchface.style.data.UserStyleWireFormat? getUserStyle();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.ComplicationScreenshotParams!>! CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.ComplicationRenderParams!>! CREATOR;
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class HeadlessWatchFaceInstanceParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
@@ -147,28 +147,28 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public class WallpaperInteractiveWatchFaceInstanceParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public WallpaperInteractiveWatchFaceInstanceParams(String, androidx.wear.watchface.data.DeviceConfig, androidx.wear.watchface.data.SystemState, androidx.wear.watchface.style.data.UserStyleWireFormat, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
+ ctor public WallpaperInteractiveWatchFaceInstanceParams(String, androidx.wear.watchface.data.DeviceConfig, androidx.wear.watchface.data.WatchUiState, androidx.wear.watchface.style.data.UserStyleWireFormat, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
method public int describeContents();
method public androidx.wear.watchface.data.DeviceConfig getDeviceConfig();
method public java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>? getIdAndComplicationDataWireFormats();
method public String getInstanceId();
- method public androidx.wear.watchface.data.SystemState getSystemState();
method public androidx.wear.watchface.style.data.UserStyleWireFormat getUserStyle();
+ method public androidx.wear.watchface.data.WatchUiState getWatchUiState();
method public void setIdAndComplicationDataWireFormats(java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
method public void setUserStyle(androidx.wear.watchface.style.data.UserStyleWireFormat);
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams!>! CREATOR;
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class WatchfaceScreenshotParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public WatchfaceScreenshotParams(androidx.wear.watchface.data.RenderParametersWireFormat, long, androidx.wear.watchface.style.data.UserStyleWireFormat?, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class WatchFaceRenderParams implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+ ctor public WatchFaceRenderParams(androidx.wear.watchface.data.RenderParametersWireFormat, long, androidx.wear.watchface.style.data.UserStyleWireFormat?, java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>?);
method public int describeContents();
method public long getCalendarTimeMillis();
method public java.util.List<androidx.wear.watchface.data.IdAndComplicationDataWireFormat!>? getIdAndComplicationDatumWireFormats();
method public androidx.wear.watchface.data.RenderParametersWireFormat getRenderParametersWireFormat();
method public androidx.wear.watchface.style.data.UserStyleWireFormat? getUserStyle();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WatchfaceScreenshotParams!>! CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.wear.watchface.control.data.WatchFaceRenderParams!>! CREATOR;
}
}
@@ -255,13 +255,13 @@
field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.RenderParametersWireFormat!>! CREATOR;
}
- @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class SystemState implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public SystemState(boolean, int);
+ @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public final class WatchUiState implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
+ ctor public WatchUiState(boolean, int);
method public int describeContents();
method public boolean getInAmbientMode();
method public int getInterruptionFilter();
method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.SystemState!>! CREATOR;
+ field public static final android.os.Parcelable.Creator<androidx.wear.watchface.data.WatchUiState!>! CREATOR;
}
}
@@ -284,8 +284,7 @@
package androidx.wear.watchface.style.data {
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class BooleanOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public BooleanOptionWireFormat(String, boolean);
- field @androidx.versionedparcelable.ParcelField(2) public boolean mValue;
+ ctor public BooleanOptionWireFormat(byte[]);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class BooleanUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -306,7 +305,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ComplicationsOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public ComplicationsOptionWireFormat(String, CharSequence, android.graphics.drawable.Icon?, androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![]);
+ ctor public ComplicationsOptionWireFormat(byte[], CharSequence, android.graphics.drawable.Icon?, androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![]);
field @androidx.versionedparcelable.ParcelField(100) public androidx.wear.watchface.style.data.ComplicationOverlayWireFormat![] mComplicationOverlays;
field @androidx.versionedparcelable.ParcelField(2) public CharSequence mDisplayName;
field @androidx.versionedparcelable.ParcelField(3) public android.graphics.drawable.Icon? mIcon;
@@ -317,7 +316,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class CustomValueOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public CustomValueOptionWireFormat(String);
+ ctor public CustomValueOptionWireFormat(byte[]);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class CustomValueUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -325,8 +324,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class DoubleRangeOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public DoubleRangeOptionWireFormat(String, double);
- field @androidx.versionedparcelable.ParcelField(2) public double mValue;
+ ctor public DoubleRangeOptionWireFormat(byte[]);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class DoubleRangeUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -334,7 +332,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class ListOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public ListOptionWireFormat(String, CharSequence, android.graphics.drawable.Icon?);
+ ctor public ListOptionWireFormat(byte[], CharSequence, android.graphics.drawable.Icon?);
field @androidx.versionedparcelable.ParcelField(2) public CharSequence mDisplayName;
field @androidx.versionedparcelable.ParcelField(3) public android.graphics.drawable.Icon? mIcon;
}
@@ -344,8 +342,7 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class LongRangeOptionWireFormat extends androidx.wear.watchface.style.data.OptionWireFormat {
- ctor public LongRangeOptionWireFormat(String, long);
- field @androidx.versionedparcelable.ParcelField(2) public long mValue;
+ ctor public LongRangeOptionWireFormat(byte[]);
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class LongRangeUserStyleSettingWireFormat extends androidx.wear.watchface.style.data.UserStyleSettingWireFormat {
@@ -353,11 +350,11 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class OptionWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public OptionWireFormat(String);
+ ctor public OptionWireFormat(byte[]);
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<androidx.wear.watchface.style.data.OptionWireFormat!>! CREATOR;
- field @androidx.versionedparcelable.ParcelField(1) public String mId;
+ field @androidx.versionedparcelable.ParcelField(1) public byte[] mId;
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize public class UserStyleSchemaWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
@@ -383,11 +380,11 @@
}
@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @androidx.versionedparcelable.VersionedParcelize(allowSerialization=true) public class UserStyleWireFormat implements android.os.Parcelable androidx.versionedparcelable.VersionedParcelable {
- ctor public UserStyleWireFormat(java.util.Map<java.lang.String!,java.lang.String!>);
+ ctor public UserStyleWireFormat(java.util.Map<java.lang.String!,byte[]!>);
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<androidx.wear.watchface.style.data.UserStyleWireFormat!>! CREATOR;
- field @androidx.versionedparcelable.ParcelField(1) public java.util.Map<java.lang.String!,java.lang.String!> mUserStyle;
+ field @androidx.versionedparcelable.ParcelField(1) public java.util.Map<java.lang.String!,byte[]!> mUserStyle;
}
}
diff --git a/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl b/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
index d0ca805..eacc2ff 100644
--- a/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/android/support/wearable/watchface/IWatchFaceService.aidl
@@ -31,12 +31,12 @@
interface IWatchFaceService {
// IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
// in the future to remain binary backwards compatible.
- // Next Id: 8
+ // Next Id: 9
/**
* API version number. This should be incremented every time a new method is added.
*/
- const int WATCHFACE_SERVICE_API_VERSION = 3;
+ const int WATCHFACE_SERVICE_API_VERSION = 4;
/**
* Requests that the style for the provided watch face be set to the given style.
@@ -97,4 +97,7 @@
* @since API version 0.
*/
int getApiVersion() = 7;
+
+ /** Reserved. Do not use. */
+ void reserved8() = 8;
}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
index 2fd8652..af5245c 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IHeadlessWatchFace.aidl
@@ -16,8 +16,8 @@
package androidx.wear.watchface.control;
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams;
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
+import androidx.wear.watchface.control.data.ComplicationRenderParams;
+import androidx.wear.watchface.control.data.WatchFaceRenderParams;
import androidx.wear.watchface.data.IdAndComplicationStateWireFormat;
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat;
@@ -73,24 +73,24 @@
* Watch Face.
*
* @since API version 1.
- * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
+ * @param params The {@link WatchFaceRenderParams} for this screenshot.
* @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
* face with the requested settings
* TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
*/
- Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 5;
+ Bundle renderWatchFaceToBitmap(in WatchFaceRenderParams params) = 5;
/**
* Request for a {@link Bundle} containing a WebP compressed shared memory backed {@link Bitmap}
* (see {@link SharedMemoryImage#ashmemCompressedImageBundleToBitmap}).
*
* @since API version 1.
- * @param params The {@link ComplicationScreenshotParams} for this screenshot.
+ * @param params The {@link ComplicationRenderParams} for this screenshot.
* @return A bundle containing a compressed shared memory backed {@link Bitmap} of the
* complication with the requested settings
* TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
*/
- Bundle takeComplicationScreenshot(in ComplicationScreenshotParams params) = 6;
+ Bundle renderComplicationToBitmap(in ComplicationRenderParams params) = 6;
/**
* Releases the watch face instance. It is an error to issue any further commands on any AIDLs
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
similarity index 62%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
index 001c086..ed3d3fb 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
@@ -16,29 +16,46 @@
package androidx.wear.watchface.control;
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
-import androidx.wear.watchface.data.SystemState;
+import android.support.wearable.watchface.accessibility.ContentDescriptionLabel;
+import androidx.wear.watchface.control.data.WatchFaceRenderParams;
+import androidx.wear.watchface.data.WatchUiState;
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat;
import androidx.wear.watchface.data.IdAndComplicationStateWireFormat;
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat;
import androidx.wear.watchface.style.data.UserStyleWireFormat;
/**
- * Interface for interacting with an interactive instance of a watch face from WCS. SysUI can also
- * control the same instance via {@link IInteractiveWatchFaceSysUI}.
+ * Interface for interacting with an interactive instance of a watch face.
*
* @hide
*/
-interface IInteractiveWatchFaceWCS {
+interface IInteractiveWatchFace {
// IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
// in the future to remain binary backwards compatible.
- // Next Id: 13
+ // Next Id: 17
/**
* API version number. This should be incremented every time a new method is added.
*/
const int API_VERSION = 1;
+ /** Indicates a "down" touch event on the watch face. */
+ const int TAP_TYPE_DOWN = 0;
+
+ /**
+ * Indicates that a previous {@link #TAP_TYPE_DOWN} event has been canceled. This generally
+ * happens when the watch face is touched but then a move or long press occurs.
+ */
+ const int TAP_TYPE_CANCEL = 1;
+
+ /**
+ * Indicates that an "up" event on the watch face has occurred that has not been consumed by
+ * another activity. A {@link #TAP_TYPE_DOWN} always occur first. This event will not occur if a
+ * {@link #TAP_TYPE_CANCEL} is sent.
+ *
+ */
+ const int TAP_TYPE_UP = 2;
+
/**
* Returns the version number for this API which the client can use to determine which methods
* are available.
@@ -59,7 +76,8 @@
*
* @since API version 1.
*/
- oneway void updateComplicationData(in List<IdAndComplicationDataWireFormat> complicationData) = 4;
+ oneway void updateComplicationData(
+ in List<IdAndComplicationDataWireFormat> complicationData) = 4;
/**
* Renames this instance to newInstanceId, sets the current user style
@@ -68,7 +86,8 @@
*
* @since API version 1.
*/
- oneway void updateInstance(in String newInstanceId, in UserStyleWireFormat userStyle) = 5;
+ oneway void updateWatchfaceInstance(
+ in String newInstanceId, in UserStyleWireFormat userStyle) = 5;
/**
* Returns the reference preview time for this watch face in milliseconds since the epoch.
@@ -101,12 +120,12 @@
* calendarTimeMillis.
*
* @since API version 1.
- * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
+ * @param params The {@link WatchFaceRenderParams} for this screenshot.
* @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
* face with the requested settings
* TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
*/
- Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 10;
+ Bundle renderWatchFaceToBitmap(in WatchFaceRenderParams params) = 10;
/**
* If there's no {@link IInteractiveWatchFaceSysUI} holding a reference then the
@@ -123,4 +142,37 @@
* @since API version 1.
*/
oneway void bringAttentionToComplication(in int complicationId) = 12;
+
+ /**
+ * Forwards a touch event for the WatchFace to process.
+ *
+ * @param xPos X Coordinate of the touch event
+ * @param yPos Y Coordinate of the touch event
+ * @param tapType One of {@link #TAP_TYPE_DOWN}, {@link #TAP_TYPE_CANCEL}, {@link #TAP_TYPE_UP}
+ * @since API version 1.
+ */
+ oneway void sendTouchEvent(in int xPos, in int yPos, in int tapType) = 13;
+
+ /**
+ * Called periodically when the watch is in ambient mode to update the watchface.
+ *
+ * @since API version 1.
+ */
+ oneway void ambientTickUpdate() = 14;
+
+ /**
+ * Sends the current {@link WatchUiState} to the Watch Face.
+ *
+ * @since API version 1.
+ */
+ oneway void setWatchUiState(in WatchUiState watchUiState) = 15;
+
+ /**
+ * Gets the labels to be read aloud by screen readers. The results will change depending on the
+ * current style and complications. Note the labes include the central time piece in addition
+ * to any complications.
+ *
+ * @since API version 1.
+ */
+ ContentDescriptionLabel[] getContentDescriptionLabels() = 16;
}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl
deleted file mode 100644
index dd86be4..0000000
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFaceSysUI.aidl
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.control;
-
-import android.support.wearable.watchface.accessibility.ContentDescriptionLabel;
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams;
-import androidx.wear.watchface.data.SystemState;
-import androidx.wear.watchface.style.data.UserStyleWireFormat;
-
-/**
- * Interface for interacting with an interactive instance of a watch face from SysUI. WCS can also
- * control the same instance via {@link IInteractiveWatchFaceWCS}.
- *
- * @hide
- */
-interface IInteractiveWatchFaceSysUI {
- // IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
- // in the future to remain binary backwards compatible.
- // Next Id: 10
-
- /**
- * API version number. This should be incremented every time a new method is added.
- */
- const int API_VERSION = 1;
-
- /** Indicates a "down" touch event on the watch face. */
- const int TAP_TYPE_TOUCH = 0;
-
- /**
- * Indicates that a previous TAP_TYPE_TOUCH event has been canceled. This generally happens when
- * the watch face is touched but then a move or long press occurs.
- */
- const int TAP_TYPE_TOUCH_CANCEL = 1;
-
- /**
- * Indicates that an "up" event on the watch face has occurred that has not been consumed by
- * another activity. A TAP_TYPE_TOUCH always occur first. This event will not occur if a
- * TAP_TYPE_TOUCH_CANCEL is sent.
- *
- */
- const int TAP_TYPE_TAP = 2;
-
- /**
- * Returns the version number for this API which the client can use to determine which methods
- * are available.
- *
- * @since API version 1.
- */
- int getApiVersion() = 1;
-
- /**
- * Returns the ID parameter set at creation.
- *
- * @since API version 1.
- */
- String getInstanceId() = 2;
-
- /**
- * Returns the reference preview time for this watch face in milliseconds since the epoch.
- *
- * @since API version 1.
- */
- long getPreviewReferenceTimeMillis() = 3;
-
- /**
- * Forwards a touch event for the WatchFace to process.
- *
- * @param xPos X Coordinate of the touch event
- * @param yPos Y Coordinate of the touch event
- * @param tapType One of {@link #TAP_TYPE_TOUCH}, {@link #TAP_TYPE_TOUCH_CANCEL},
- * {@link #TAP_TYPE_TAP}
- * @since API version 1.
- */
- oneway void sendTouchEvent(in int xPos, in int yPos, in int tapType) = 4;
-
- /**
- * Called periodically when the watch is in ambient mode to update the watchface.
- *
- * @since API version 1.
- */
- oneway void ambientTickUpdate() = 5;
-
- /**
- * Sends the current system state to the Watch Face.
- *
- * @since API version 1.
- */
- oneway void setSystemState(in SystemState systemState) = 6;
-
- /**
- * Request for a {@link Bundle} containing a WebP compressed shared memory backed {@link Bitmap}
- * (see {@link SharedMemoryImage#ashmemCompressedImageBundleToBitmap}) with a screenshot of the
- * Watch Face with the specified DrawMode (see {@link androidx.wear.watchface.DrawMode}) and
- * calendarTimeMillis.
- *
- * @since API version 1.
- * @param params The {@link WatchfaceScreenshotParams} for this screenshot.
- * @return A bundle containing a compressed shared memory backed {@link Bitmap} of the watch
- * face with the requested settings
- * TODO(alexclarke): Refactor to return a parcelable rather than a bundle.
- */
- Bundle takeWatchFaceScreenshot(in WatchfaceScreenshotParams params) = 7;
-
- /**
- * Gets the labels to be read aloud by screen readers. The results will change depending on the
- * current style and complications. Note the labes include the central time piece in addition
- * to any complications.
- *
- * @since API version 1.
- */
- ContentDescriptionLabel[] getContentDescriptionLabels() = 8;
-
- /**
- * If there's no {@link IInteractiveWatchFaceWCS} holding a reference then the instance is
- * diposed of. It is an error to issue any further AIDL commands via this interface.
- *
- * @since API version 1.
- */
- oneway void release() = 9;
-}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
similarity index 85%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
index 42fbc22..005eac3 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFaceWCS.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IPendingInteractiveWatchFace.aidl
@@ -16,14 +16,14 @@
package androidx.wear.watchface.control;
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IInteractiveWatchFace;
/**
* Callback issued when {@link IInteractiveWatchFaceWcs} has been created.
*
* @hide
*/
-interface IPendingInteractiveWatchFaceWCS {
+interface IPendingInteractiveWatchFace {
// IMPORTANT NOTE: All methods must be given an explicit transaction id that must never change
// in the future to remain binary backwards compatible.
// Next Id: 8
@@ -42,6 +42,5 @@
int getApiVersion() = 1;
/** Called by the watchface when {@link IInteractiveWatchFaceWcs} has been created. */
- oneway void onInteractiveWatchFaceWcsCreated(
- in IInteractiveWatchFaceWCS iInteractiveWatchFaceWcs) = 2;
+ oneway void onInteractiveWatchFaceCreated(in IInteractiveWatchFace iInteractiveWatchFace) = 2;
}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
index 49d95f3..103b961 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/IWatchFaceControlService.aidl
@@ -16,10 +16,9 @@
package androidx.wear.watchface.control;
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI;
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IInteractiveWatchFace;
import androidx.wear.watchface.control.IHeadlessWatchFace;
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS;
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace;
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams;
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams;
import androidx.wear.watchface.editor.IEditorService;
@@ -48,11 +47,11 @@
int getApiVersion() = 1;
/**
- * Gets the {@link IInteractiveWatchFaceSysUI} corresponding to the id of an existing watch
+ * Gets the {@link IInteractiveWatchFace} corresponding to the id of an existing watch
* face instance, or null if there is no such instance. The id is set when the instance is
* created, see {@link WallpaperInteractiveWatchFaceInstanceParams}.
*/
- IInteractiveWatchFaceSysUI getInteractiveWatchFaceInstanceSysUI(in String id) = 2;
+ IInteractiveWatchFace getInteractiveWatchFaceInstance(in String id) = 2;
/**
* Creates a headless WatchFace instance for the specified watchFaceName and returns an {@link
@@ -70,7 +69,7 @@
in HeadlessWatchFaceInstanceParams params) = 3;
/**
- * Either returns an existing IInteractiveWatchFaceWCS instance or othrwise schedules
+ * Either returns an existing IInteractiveWatchFace instance or othrwise schedules
* creation of an IInteractiveWatchFace for the next time the wallpaper service connects and
* calls WatchFaceService.onCreateEngine.
*
@@ -78,13 +77,13 @@
* instance to be made when WatchFaceService.onCreateEngine is called. If an existing
* instance is returned this callback won't fire.
* @param callback Callback fired when the wathface is created.
- * @return The existing {@link IInteractiveWatchFaceWCS} or null in which the callback will fire
+ * @return The existing {@link IInteractiveWatchFace} or null in which the callback will fire
* the next time the wallpaper service connects and calls WatchFaceService.onCreateEngine.
* @since API version 1.
*/
- IInteractiveWatchFaceWCS getOrCreateInteractiveWatchFaceWCS(
+ IInteractiveWatchFace getOrCreateInteractiveWatchFace(
in WallpaperInteractiveWatchFaceInstanceParams params,
- in IPendingInteractiveWatchFaceWCS callback) = 4;
+ in IPendingInteractiveWatchFace callback) = 4;
/**
* Returns the {@link IEditorService}
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
similarity index 94%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
index decdabc..299880b 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationRenderParams.aidl
@@ -17,4 +17,4 @@
package androidx.wear.watchface.control.data;
/** @hide */
-parcelable WatchfaceScreenshotParams;
+parcelable ComplicationRenderParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl
deleted file mode 100644
index 3f8b02c..0000000
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/ComplicationScreenshotParams.aidl
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.wear.watchface.control.data;
-
-/** @hide */
-parcelable ComplicationScreenshotParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
similarity index 94%
copy from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
copy to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
index decdabc..0577ccf 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/control/data/WatchFaceRenderParams.aidl
@@ -17,4 +17,4 @@
package androidx.wear.watchface.control.data;
/** @hide */
-parcelable WatchfaceScreenshotParams;
+parcelable WatchFaceRenderParams;
diff --git a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
similarity index 96%
rename from wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl
rename to wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
index e648e52..d7a8888 100644
--- a/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/SystemState.aidl
+++ b/wear/wear-watchface-data/src/main/aidl/androidx/wear/watchface/data/WatchUiState.aidl
@@ -17,4 +17,4 @@
package androidx.wear.watchface.data;
/** @hide */
-parcelable SystemState;
+parcelable WatchUiState;
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
similarity index 86%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
index a57438c..f3a9b0d 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationScreenshotParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/ComplicationRenderParams.java
@@ -32,14 +32,14 @@
import androidx.wear.watchface.style.data.UserStyleWireFormat;
/**
- * Parameters for the various AIDL takeComplicationScreenshot commands.
+ * Parameters for the various AIDL renderComplicationToBitmap commands.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize
@SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public class ComplicationScreenshotParams implements VersionedParcelable, Parcelable {
+public class ComplicationRenderParams implements VersionedParcelable, Parcelable {
/** ID of the complication we want to take a screen short of. */
@ParcelField(1)
@@ -72,9 +72,9 @@
UserStyleWireFormat mUserStyle;
/** Used by VersionedParcelable. */
- ComplicationScreenshotParams() {}
+ ComplicationRenderParams() {}
- public ComplicationScreenshotParams(
+ public ComplicationRenderParams(
int complicationId,
@NonNull RenderParametersWireFormat renderParametersWireFormats,
long calendarTimeMillis,
@@ -122,17 +122,17 @@
return 0;
}
- public static final Parcelable.Creator<ComplicationScreenshotParams> CREATOR =
- new Parcelable.Creator<ComplicationScreenshotParams>() {
+ public static final Parcelable.Creator<ComplicationRenderParams> CREATOR =
+ new Parcelable.Creator<ComplicationRenderParams>() {
@Override
- public ComplicationScreenshotParams createFromParcel(Parcel source) {
+ public ComplicationRenderParams createFromParcel(Parcel source) {
return ParcelUtils.fromParcelable(
source.readParcelable(getClass().getClassLoader()));
}
@Override
- public ComplicationScreenshotParams[] newArray(int size) {
- return new ComplicationScreenshotParams[size];
+ public ComplicationRenderParams[] newArray(int size) {
+ return new ComplicationRenderParams[size];
}
};
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index 0f81640..a9a3729 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -30,7 +30,7 @@
import androidx.wear.watchface.control.IWatchFaceControlService;
import androidx.wear.watchface.data.DeviceConfig;
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat;
-import androidx.wear.watchface.data.SystemState;
+import androidx.wear.watchface.data.WatchUiState;
import androidx.wear.watchface.style.data.UserStyleWireFormat;
import java.util.List;
@@ -56,10 +56,10 @@
@NonNull
DeviceConfig mDeviceConfig;
- /** The {@link SystemState} for the host wearable. */
+ /** The {@link WatchUiState} for the host wearable. */
@ParcelField(3)
@NonNull
- SystemState mSystemState;
+ WatchUiState mWatchUiState;
/** The initial {@link UserStyleWireFormat}. */
@ParcelField(4)
@@ -78,12 +78,12 @@
public WallpaperInteractiveWatchFaceInstanceParams(
@NonNull String instanceId,
@NonNull DeviceConfig deviceConfig,
- @NonNull SystemState systemState,
+ @NonNull WatchUiState watchUiState,
@NonNull UserStyleWireFormat userStyle,
@Nullable List<IdAndComplicationDataWireFormat> idAndComplicationDataWireFormats) {
mInstanceId = instanceId;
mDeviceConfig = deviceConfig;
- mSystemState = systemState;
+ mWatchUiState = watchUiState;
mUserStyle = userStyle;
mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
}
@@ -99,8 +99,8 @@
}
@NonNull
- public SystemState getSystemState() {
- return mSystemState;
+ public WatchUiState getWatchUiState() {
+ return mWatchUiState;
}
@NonNull
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
similarity index 87%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
index dc5da8f..291fd02 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchfaceScreenshotParams.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/control/data/WatchFaceRenderParams.java
@@ -41,7 +41,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize
@SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public class WatchfaceScreenshotParams implements VersionedParcelable, Parcelable {
+public class WatchFaceRenderParams implements VersionedParcelable, Parcelable {
/** The {@link RenderParametersWireFormat} to render with. */
@ParcelField(1)
@NonNull
@@ -68,9 +68,9 @@
List<IdAndComplicationDataWireFormat> mIdAndComplicationDatumWireFormats;
/** Used by VersionedParcelable. */
- WatchfaceScreenshotParams() {}
+ WatchFaceRenderParams() {}
- public WatchfaceScreenshotParams(
+ public WatchFaceRenderParams(
@NonNull RenderParametersWireFormat renderParametersWireFormats,
long calendarTimeMillis,
@Nullable UserStyleWireFormat userStyle,
@@ -112,17 +112,17 @@
return 0;
}
- public static final Parcelable.Creator<WatchfaceScreenshotParams> CREATOR =
- new Parcelable.Creator<WatchfaceScreenshotParams>() {
+ public static final Parcelable.Creator<WatchFaceRenderParams> CREATOR =
+ new Parcelable.Creator<WatchFaceRenderParams>() {
@Override
- public WatchfaceScreenshotParams createFromParcel(Parcel source) {
+ public WatchFaceRenderParams createFromParcel(Parcel source) {
return ParcelUtils.fromParcelable(
source.readParcelable(getClass().getClassLoader()));
}
@Override
- public WatchfaceScreenshotParams[] newArray(int size) {
- return new WatchfaceScreenshotParams[size];
+ public WatchFaceRenderParams[] newArray(int size) {
+ return new WatchFaceRenderParams[size];
}
};
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
similarity index 79%
rename from wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java
rename to wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
index 996d953..78920fe 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/SystemState.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/data/WatchUiState.java
@@ -28,14 +28,14 @@
import androidx.versionedparcelable.VersionedParcelize;
/**
- * Data sent over AIDL for {@link IWatchFaceCommand#setSystemState}.
+ * Data sent over AIDL for {@link IWatchFaceCommand#setWatchUiState}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize(allowSerialization = true)
@SuppressLint("BanParcelableUsage") // TODO(b/169214666): Remove Parcelable
-public final class SystemState implements VersionedParcelable, Parcelable {
+public final class WatchUiState implements VersionedParcelable, Parcelable {
@ParcelField(1)
boolean mInAmbientMode;
@@ -43,9 +43,9 @@
int mInterruptionFilter;
/** Used by VersionedParcelable. */
- SystemState() {}
+ WatchUiState() {}
- public SystemState(
+ public WatchUiState(
boolean inAmbientMode,
int interruptionFilter) {
mInAmbientMode = inAmbientMode;
@@ -60,7 +60,7 @@
return mInterruptionFilter;
}
- /** Serializes this SystemState to the specified {@link Parcel}. */
+ /** Serializes this WatchUiState to the specified {@link Parcel}. */
@Override
public void writeToParcel(@NonNull Parcel parcel, int flags) {
parcel.writeParcelable(ParcelUtils.toParcelable(this), flags);
@@ -71,17 +71,17 @@
return 0;
}
- public static final Parcelable.Creator<SystemState> CREATOR =
- new Parcelable.Creator<SystemState>() {
+ public static final Parcelable.Creator<WatchUiState> CREATOR =
+ new Parcelable.Creator<WatchUiState>() {
@Override
- public SystemState createFromParcel(Parcel source) {
+ public WatchUiState createFromParcel(Parcel source) {
return ParcelUtils.fromParcelable(
source.readParcelable(getClass().getClassLoader()));
}
@Override
- public SystemState[] newArray(int size) {
- return new SystemState[size];
+ public WatchUiState[] newArray(int size) {
+ return new WatchUiState[size];
}
};
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
index 5ffdb73..3bcb13a 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/BooleanOptionWireFormat.java
@@ -18,7 +18,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
/**
@@ -29,14 +28,11 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize
public class BooleanOptionWireFormat extends OptionWireFormat {
- @ParcelField(2)
- public boolean mValue;
BooleanOptionWireFormat() {
}
- public BooleanOptionWireFormat(@NonNull String id, boolean value) {
+ public BooleanOptionWireFormat(@NonNull byte[] id) {
super(id);
- this.mValue = value;
}
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
index b73af58..a220a66 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ComplicationsOptionWireFormat.java
@@ -56,7 +56,7 @@
}
public ComplicationsOptionWireFormat(
- @NonNull String id,
+ @NonNull byte[] id,
@NonNull CharSequence displayName,
@Nullable Icon icon,
@NonNull ComplicationOverlayWireFormat[]
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
index df52b64b..63ce7ad 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/CustomValueOptionWireFormat.java
@@ -32,7 +32,7 @@
CustomValueOptionWireFormat() {
}
- public CustomValueOptionWireFormat(@NonNull String id) {
+ public CustomValueOptionWireFormat(@NonNull byte[] id) {
super(id);
}
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
index efff12b..a840917 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/DoubleRangeOptionWireFormat.java
@@ -18,7 +18,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
/**
@@ -30,15 +29,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize
public class DoubleRangeOptionWireFormat extends OptionWireFormat {
- /* The value for this option. Must be within the range [minimumValue .. maximumValue]. */
- @ParcelField(2)
- public double mValue;
-
DoubleRangeOptionWireFormat() {
}
- public DoubleRangeOptionWireFormat(@NonNull String id, double value) {
+ public DoubleRangeOptionWireFormat(@NonNull byte[] id) {
super(id);
- this.mValue = value;
}
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
index 1468064..b81eb21 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/ListOptionWireFormat.java
@@ -47,7 +47,7 @@
}
public ListOptionWireFormat(
- @NonNull String id,
+ @NonNull byte[] id,
@NonNull CharSequence displayName,
@Nullable Icon icon
) {
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
index 78bc6be..27ffb72 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/LongRangeOptionWireFormat.java
@@ -18,7 +18,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.versionedparcelable.ParcelField;
import androidx.versionedparcelable.VersionedParcelize;
/**
@@ -30,15 +29,10 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@VersionedParcelize
public class LongRangeOptionWireFormat extends OptionWireFormat {
- /* The value for this option. Must be within the range [minimumValue .. maximumValue]. */
- @ParcelField(2)
- public long mValue;
-
LongRangeOptionWireFormat() {
}
- public LongRangeOptionWireFormat(@NonNull String id, long value) {
+ public LongRangeOptionWireFormat(@NonNull byte[] id) {
super(id);
- this.mValue = value;
}
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
index af2ff2b..2d2190e 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/OptionWireFormat.java
@@ -39,12 +39,12 @@
/** Identifier for the option, must be unique within the UserStyleCategory. */
@ParcelField(1)
@NonNull
- public String mId = "";
+ public byte[] mId = new byte[0];
OptionWireFormat() {
}
- public OptionWireFormat(@NonNull String id) {
+ public OptionWireFormat(@NonNull byte[] id) {
mId = id;
}
diff --git a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
index 4d69501..0e5f3b8 100644
--- a/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
+++ b/wear/wear-watchface-data/src/main/java/androidx/wear/watchface/style/data/UserStyleWireFormat.java
@@ -40,11 +40,11 @@
@ParcelField(1)
@NonNull
/** Map from user style setting id to user style option id. */
- public Map<String, String> mUserStyle = new HashMap<>();
+ public Map<String, byte[]> mUserStyle = new HashMap<>();
UserStyleWireFormat() {}
- public UserStyleWireFormat(@NonNull Map<String, String> userStyle) {
+ public UserStyleWireFormat(@NonNull Map<String, byte[]> userStyle) {
mUserStyle = userStyle;
}
diff --git a/wear/wear-watchface-editor/api/current.txt b/wear/wear-watchface-editor/api/current.txt
index 8dd9c55..5e23ca2 100644
--- a/wear/wear-watchface-editor/api/current.txt
+++ b/wear/wear-watchface-editor/api/current.txt
@@ -2,16 +2,17 @@
package androidx.wear.watchface.editor {
public final class EditorRequest {
- ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+ ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
method public String getEditorPackageName();
- method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+ method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
method public android.content.ComponentName getWatchFaceComponentName();
- method public String? getWatchFaceInstanceId();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
property public final String editorPackageName;
- property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+ property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
property public final android.content.ComponentName watchFaceComponentName;
- property public final String? watchFaceInstanceId;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
}
@@ -22,35 +23,35 @@
public abstract class EditorSession implements java.lang.AutoCloseable {
ctor public EditorSession();
method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
method public abstract Integer? getBackgroundComplicationId();
method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
- method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public abstract String? getInstanceId();
+ method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public abstract long getPreviewReferenceTimeMillis();
method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public abstract android.content.ComponentName getWatchFaceComponentName();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public final boolean isCommitChangesOnClose();
- method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method @UiThread public final void setCommitChangesOnClose(boolean p);
method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
- method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract Integer? backgroundComplicationId;
property @UiThread public final boolean commitChangesOnClose;
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String? instanceId;
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyle userStyle;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public abstract android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
}
public static final class EditorSession.Companion {
method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
}
public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/api/public_plus_experimental_current.txt b/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
index 8dd9c55..5e23ca2 100644
--- a/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-editor/api/public_plus_experimental_current.txt
@@ -2,16 +2,17 @@
package androidx.wear.watchface.editor {
public final class EditorRequest {
- ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+ ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
method public String getEditorPackageName();
- method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+ method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
method public android.content.ComponentName getWatchFaceComponentName();
- method public String? getWatchFaceInstanceId();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
property public final String editorPackageName;
- property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+ property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
property public final android.content.ComponentName watchFaceComponentName;
- property public final String? watchFaceInstanceId;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
}
@@ -22,35 +23,35 @@
public abstract class EditorSession implements java.lang.AutoCloseable {
ctor public EditorSession();
method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
method public abstract Integer? getBackgroundComplicationId();
method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
- method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public abstract String? getInstanceId();
+ method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public abstract long getPreviewReferenceTimeMillis();
method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public abstract android.content.ComponentName getWatchFaceComponentName();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public final boolean isCommitChangesOnClose();
- method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method @UiThread public final void setCommitChangesOnClose(boolean p);
method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
- method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract Integer? backgroundComplicationId;
property @UiThread public final boolean commitChangesOnClose;
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String? instanceId;
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyle userStyle;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public abstract android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
}
public static final class EditorSession.Companion {
method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
}
public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/api/restricted_current.txt b/wear/wear-watchface-editor/api/restricted_current.txt
index c3fa11c..a42c76e 100644
--- a/wear/wear-watchface-editor/api/restricted_current.txt
+++ b/wear/wear-watchface-editor/api/restricted_current.txt
@@ -7,10 +7,10 @@
method public Integer? getBackgroundComplicationId();
method protected final boolean getClosed();
method public Integer? getComplicationIdAt(@Px int x, @Px int y);
- method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> $completion);
+ method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> $completion);
method public final kotlinx.coroutines.CoroutineScope getCoroutineScope();
method protected final boolean getForceClosed();
- method public suspend Object? launchComplicationProviderChooser(int p, kotlin.coroutines.Continuation<? super java.lang.Boolean> $completion);
+ method public suspend Object? openComplicationProviderChooser(int p, kotlin.coroutines.Continuation<? super java.lang.Boolean> $completion);
method @UiThread protected abstract void releaseResources();
method protected final void requireNotClosed();
method protected final void setClosed(boolean p);
@@ -22,16 +22,17 @@
}
public final class EditorRequest {
- ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, String? watchFaceInstanceId, java.util.Map<java.lang.String,java.lang.String>? initialUserStyle);
+ ctor @RequiresApi(android.os.Build.VERSION_CODES.R) public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle, @RequiresApi(android.os.Build.VERSION_CODES.R) androidx.wear.watchface.client.WatchFaceId watchFaceId);
+ ctor public EditorRequest(android.content.ComponentName watchFaceComponentName, String editorPackageName, androidx.wear.watchface.style.UserStyleData? initialUserStyle);
method public static androidx.wear.watchface.editor.EditorRequest? createFromIntent(android.content.Intent intent);
method public String getEditorPackageName();
- method public java.util.Map<java.lang.String,java.lang.String>? getInitialUserStyle();
+ method public androidx.wear.watchface.style.UserStyleData? getInitialUserStyle();
method public android.content.ComponentName getWatchFaceComponentName();
- method public String? getWatchFaceInstanceId();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
property public final String editorPackageName;
- property public final java.util.Map<java.lang.String,java.lang.String>? initialUserStyle;
+ property public final androidx.wear.watchface.style.UserStyleData? initialUserStyle;
property public final android.content.ComponentName watchFaceComponentName;
- property public final String? watchFaceInstanceId;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public final androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorRequest.Companion Companion;
}
@@ -42,35 +43,35 @@
public abstract class EditorSession implements java.lang.AutoCloseable {
ctor public EditorSession();
method @RequiresApi(27) @UiThread public static final androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public static final kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public static final suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity p, android.content.Intent activity, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> editIntent);
method public abstract Integer? getBackgroundComplicationId();
method @UiThread public abstract Integer? getComplicationIdAt(@Px int x, @Px int y);
- method @UiThread public abstract suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public abstract String? getInstanceId();
+ method @UiThread public abstract suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public abstract long getPreviewReferenceTimeMillis();
method public abstract androidx.wear.watchface.style.UserStyle getUserStyle();
method public abstract androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public abstract android.content.ComponentName getWatchFaceComponentName();
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public final boolean isCommitChangesOnClose();
- method @UiThread public abstract suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @UiThread public abstract android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method @UiThread public final void setCommitChangesOnClose(boolean p);
method public abstract void setUserStyle(androidx.wear.watchface.style.UserStyle p);
- method @UiThread public abstract android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract Integer? backgroundComplicationId;
property @UiThread public final boolean commitChangesOnClose;
- property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public abstract String? instanceId;
+ property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public abstract long previewReferenceTimeMillis;
property public abstract androidx.wear.watchface.style.UserStyle userStyle;
property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public abstract android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public abstract androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.EditorSession.Companion Companion;
}
public static final class EditorSession.Companion {
method @RequiresApi(27) @UiThread public androidx.wear.watchface.editor.EditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
- method @UiThread public kotlinx.coroutines.Deferred<androidx.wear.watchface.editor.EditorSession> createOnWatchEditingSessionAsync(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
+ method @UiThread public suspend Object? createOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, kotlin.coroutines.Continuation<? super androidx.wear.watchface.editor.EditorSession> p);
}
public class WatchFaceEditorContract extends androidx.activity.result.contract.ActivityResultContract<androidx.wear.watchface.editor.EditorRequest,kotlin.Unit> {
diff --git a/wear/wear-watchface-editor/guava/api/current.txt b/wear/wear-watchface-editor/guava/api/current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/current.txt
+++ b/wear/wear-watchface-editor/guava/api/current.txt
@@ -7,26 +7,26 @@
method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method public Integer? getBackgroundComplicationId();
method public Integer? getComplicationIdAt(int x, int y);
- method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String? getInstanceId();
+ method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyle getUserStyle();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public android.content.ComponentName getWatchFaceComponentName();
- method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+ method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public Integer? backgroundComplicationId;
- property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public String? instanceId;
+ property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public long previewReferenceTimeMillis;
property public androidx.wear.watchface.style.UserStyle userStyle;
property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
}
diff --git a/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt b/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-editor/guava/api/public_plus_experimental_current.txt
@@ -7,26 +7,26 @@
method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method public Integer? getBackgroundComplicationId();
method public Integer? getComplicationIdAt(int x, int y);
- method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String? getInstanceId();
+ method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyle getUserStyle();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public android.content.ComponentName getWatchFaceComponentName();
- method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+ method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public Integer? backgroundComplicationId;
- property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public String? instanceId;
+ property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public long previewReferenceTimeMillis;
property public androidx.wear.watchface.style.UserStyle userStyle;
property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
}
diff --git a/wear/wear-watchface-editor/guava/api/restricted_current.txt b/wear/wear-watchface-editor/guava/api/restricted_current.txt
index 097e699..8a92560 100644
--- a/wear/wear-watchface-editor/guava/api/restricted_current.txt
+++ b/wear/wear-watchface-editor/guava/api/restricted_current.txt
@@ -7,26 +7,26 @@
method @RequiresApi(27) @UiThread public static androidx.wear.watchface.editor.ListenableEditorSession? createHeadlessEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent, androidx.wear.watchface.client.HeadlessWatchFaceClient headlessWatchFaceClient);
method public Integer? getBackgroundComplicationId();
method public Integer? getComplicationIdAt(int x, int y);
- method public suspend Object? getComplicationPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
- method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
- method public String? getInstanceId();
+ method public suspend Object? getComplicationsPreviewData(kotlin.coroutines.Continuation<? super java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>> p);
+ method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationsState();
method public com.google.common.util.concurrent.ListenableFuture<java.util.Map<java.lang.Integer,androidx.wear.complications.data.ComplicationData>> getListenableComplicationPreviewData();
method public long getPreviewReferenceTimeMillis();
method public androidx.wear.watchface.style.UserStyle getUserStyle();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public android.content.ComponentName getWatchFaceComponentName();
- method public suspend Object? launchComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId getWatchFaceId();
method @UiThread public static com.google.common.util.concurrent.ListenableFuture<androidx.wear.watchface.editor.ListenableEditorSession> listenableCreateOnWatchEditingSession(androidx.activity.ComponentActivity activity, android.content.Intent editIntent);
- method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableLaunchComplicationProviderChooser(int complicationId);
+ method public com.google.common.util.concurrent.ListenableFuture<java.lang.Boolean> listenableOpenComplicationProviderChooser(int complicationId);
+ method public suspend Object? openComplicationProviderChooser(int complicationId, kotlin.coroutines.Continuation<? super java.lang.Boolean> p);
+ method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method public void setUserStyle(androidx.wear.watchface.style.UserStyle value);
- method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public Integer? backgroundComplicationId;
- property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
- property public String? instanceId;
+ property public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationsState;
property public long previewReferenceTimeMillis;
property public androidx.wear.watchface.style.UserStyle userStyle;
property public androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
property public android.content.ComponentName watchFaceComponentName;
+ property @RequiresApi(android.os.Build.VERSION_CODES.R) public androidx.wear.watchface.client.WatchFaceId watchFaceId;
field public static final androidx.wear.watchface.editor.ListenableEditorSession.Companion Companion;
}
diff --git a/wear/wear-watchface-editor/guava/build.gradle b/wear/wear-watchface-editor/guava/build.gradle
index 86e9298..5d7afd9 100644
--- a/wear/wear-watchface-editor/guava/build.gradle
+++ b/wear/wear-watchface-editor/guava/build.gradle
@@ -45,7 +45,7 @@
name = "Android Wear Watchface Client Editor"
type = LibraryType.PUBLISHED_LIBRARY
mavenGroup = LibraryGroups.WEAR
- mavenVersion = LibraryVersions.WEAR_WATCHFACE_EDITOR
+ mavenVersion = LibraryVersions.WEAR_WATCHFACE_EDITOR_GUAVA
inceptionYear = "2021"
description = "Guava wrappers for the Androidx Wear Watchface Editor library"
}
diff --git a/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt b/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
index b59c76e..7827a94 100644
--- a/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
+++ b/wear/wear-watchface-editor/guava/src/main/java/androidx/wear/watchface/editor/ListenableEditorSession.kt
@@ -19,6 +19,7 @@
import android.content.ComponentName
import android.content.Intent
import android.graphics.Bitmap
+import android.os.Build
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.annotation.UiThread
@@ -27,6 +28,7 @@
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.client.ComplicationState
import androidx.wear.watchface.client.HeadlessWatchFaceClient
+import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
import com.google.common.util.concurrent.ListenableFuture
@@ -67,10 +69,9 @@
coroutineScope.launch {
try {
result.set(
- EditorSession.createOnWatchEditingSessionAsync(
- activity,
- editIntent
- ).await()?.let { ListenableEditorSession(it) }
+ createOnWatchEditingSession(activity, editIntent)?.let {
+ ListenableEditorSession(it)
+ }
)
} catch (e: Exception) {
result.setException(e)
@@ -105,7 +106,9 @@
override val watchFaceComponentName: ComponentName = wrappedEditorSession.watchFaceComponentName
- override val instanceId: String? = wrappedEditorSession.instanceId
+ @get:RequiresApi(Build.VERSION_CODES.R)
+ @RequiresApi(Build.VERSION_CODES.R)
+ override val watchFaceId: WatchFaceId = wrappedEditorSession.watchFaceId
override var userStyle: UserStyle
get() = wrappedEditorSession.userStyle
@@ -118,16 +121,16 @@
override val userStyleSchema: UserStyleSchema
get() = wrappedEditorSession.userStyleSchema
- override val complicationState: Map<Int, ComplicationState>
- get() = wrappedEditorSession.complicationState
+ override val complicationsState: Map<Int, ComplicationState>
+ get() = wrappedEditorSession.complicationsState
- /** [ListenableFuture] wrapper around [EditorSession.getComplicationPreviewData]. */
+ /** [ListenableFuture] wrapper around [EditorSession.getComplicationsPreviewData]. */
public fun getListenableComplicationPreviewData():
ListenableFuture<Map<Int, ComplicationData>> {
val future = ResolvableFuture.create<Map<Int, ComplicationData>>()
getCoroutineScope().launch {
try {
- future.set(wrappedEditorSession.getComplicationPreviewData())
+ future.set(wrappedEditorSession.getComplicationsPreviewData())
} catch (e: Exception) {
future.setException(e)
}
@@ -135,8 +138,8 @@
return future
}
- override suspend fun getComplicationPreviewData(): Map<Int, ComplicationData> =
- wrappedEditorSession.getComplicationPreviewData()
+ override suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData> =
+ wrappedEditorSession.getComplicationsPreviewData()
@get:SuppressWarnings("AutoBoxing")
override val backgroundComplicationId: Int?
@@ -146,25 +149,25 @@
override fun getComplicationIdAt(x: Int, y: Int): Int? =
wrappedEditorSession.getComplicationIdAt(x, y)
- override fun takeWatchFaceScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
- ): Bitmap = wrappedEditorSession.takeWatchFaceScreenshot(
+ ): Bitmap = wrappedEditorSession.renderWatchFaceToBitmap(
renderParameters,
calendarTimeMillis,
idToComplicationData
)
- /** [ListenableFuture] wrapper around [EditorSession.launchComplicationProviderChooser]. */
- public fun listenableLaunchComplicationProviderChooser(
+ /** [ListenableFuture] wrapper around [EditorSession.openComplicationProviderChooser]. */
+ public fun listenableOpenComplicationProviderChooser(
complicationId: Int
): ListenableFuture<Boolean> {
val future = ResolvableFuture.create<Boolean>()
getCoroutineScope().launch {
try {
future.set(
- wrappedEditorSession.launchComplicationProviderChooser(complicationId)
+ wrappedEditorSession.openComplicationProviderChooser(complicationId)
)
} catch (e: Exception) {
future.setException(e)
@@ -173,8 +176,8 @@
return future
}
- override suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean =
- wrappedEditorSession.launchComplicationProviderChooser(complicationId)
+ override suspend fun openComplicationProviderChooser(complicationId: Int): Boolean =
+ wrappedEditorSession.openComplicationProviderChooser(complicationId)
override fun close() {
wrappedEditorSession.close()
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
index c3b0876..ebd6b45 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ComplicationConfigFragment.kt
@@ -97,7 +97,7 @@
init {
watchFaceConfigActivity.coroutineScope.launch {
previewComplicationData =
- watchFaceConfigActivity.editorSession.getComplicationPreviewData()
+ watchFaceConfigActivity.editorSession.getComplicationsPreviewData()
setWillNotDraw(false)
}
}
@@ -159,7 +159,7 @@
// Silently do nothing if the complication is fixed. Note the user is given a visual clue
// that the complication is not editable in [Complication.drawOutline] so this is OK.
val complicationState =
- watchFaceConfigActivity.editorSession.complicationState[complicationId]!!
+ watchFaceConfigActivity.editorSession.complicationsState[complicationId]!!
if (complicationState.fixedComplicationProvider) {
return true
}
@@ -209,13 +209,13 @@
override fun onDraw(canvas: Canvas) {
val editingSession = watchFaceConfigActivity.editorSession
- val bitmap = editingSession.takeWatchFaceScreenshot(
+ val bitmap = editingSession.renderWatchFaceToBitmap(
RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
selectedComplicationId,
Color.RED
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
index e88ea1c..7f06cf8 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/ConfigFragment.kt
@@ -87,7 +87,7 @@
private fun initConfigOptions() {
val editingSession = watchFaceConfigActivity.editorSession
val hasBackgroundComplication = editingSession.backgroundComplicationId != null
- val numComplications = editingSession.complicationState.size
+ val numComplications = editingSession.complicationsState.size
val hasNonBackgroundComplication =
numComplications > if (hasBackgroundComplication) 1 else 0
val configOptions = ArrayList<ConfigOption>()
@@ -113,7 +113,7 @@
for (styleCategory in editingSession.userStyleSchema.userStyleSettings) {
configOptions.add(
ConfigOption(
- id = styleCategory.id,
+ id = styleCategory.id.value,
icon = styleCategory.icon,
title = styleCategory.displayName.toString(),
summary = styleCategory.description.toString()
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
index 2f63855..e3b783f 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/StyleConfigFragment.kt
@@ -37,6 +37,7 @@
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
import androidx.wear.watchface.style.data.UserStyleWireFormat
import androidx.wear.widget.SwipeDismissFrameLayout
@@ -101,9 +102,13 @@
when {
booleanUserStyleSetting.isNotEmpty() -> {
- booleanStyle.isChecked = userStyle[styleSetting]!!.id.toBoolean()
+ booleanStyle.isChecked = userStyle[styleSetting]?.toBooleanOption()!!.value
booleanStyle.setOnCheckedChangeListener { _, isChecked ->
- setUserStyleOption(styleSetting.getOptionForId(isChecked.toString()))
+ setUserStyleOption(
+ styleSetting.getOptionForId(
+ BooleanUserStyleSetting.BooleanOption(isChecked).id.value
+ )
+ )
}
styleOptionsList.visibility = View.GONE
styleOptionsList.isEnabled = false
@@ -163,7 +168,9 @@
) {
setUserStyleOption(
rangedStyleSetting.getOptionForId(
- (minValue + delta * progress.toFloat()).toString()
+ DoubleRangeUserStyleSetting.DoubleRangeOption(
+ minValue + delta * progress.toFloat()
+ ).id.value
)
)
}
@@ -199,13 +206,15 @@
)
userStyle = UserStyle(
- ParcelUtils.fromParcelable<UserStyleWireFormat>(
- requireArguments().getParcelable(USER_STYLE)!!
- )!!,
+ UserStyleData(
+ ParcelUtils.fromParcelable<UserStyleWireFormat>(
+ requireArguments().getParcelable(USER_STYLE)!!
+ )!!
+ ),
styleSchema
)
- styleSetting = styleSchema.userStyleSettings.first { it.id == settingId }
+ styleSetting = styleSchema.userStyleSettings.first { it.id.value == settingId }
}
internal fun setUserStyleOption(userStyleOption: UserStyleSetting.Option) {
diff --git a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
index b9db60c..6955326 100644
--- a/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
+++ b/wear/wear-watchface-editor/samples/src/main/java/androidx/wear/watchface/editor/sample/WatchFaceConfigActivity.kt
@@ -82,13 +82,12 @@
super.onCreate(savedInstanceState)
handler = Handler(Looper.getMainLooper())
coroutineScope = CoroutineScope(handler.asCoroutineDispatcher().immediate)
- val deferredEditorSession = EditorSession.createOnWatchEditingSessionAsync(
- this@WatchFaceConfigActivity,
- intent!!
- )
coroutineScope.launch {
init(
- deferredEditorSession.await()!!,
+ EditorSession.createOnWatchEditingSession(
+ this@WatchFaceConfigActivity,
+ intent!!
+ )!!,
object : FragmentController {
@SuppressLint("SyntheticAccessor")
override fun showConfigFragment() {
@@ -118,7 +117,7 @@
@SuppressWarnings("deprecation")
override suspend fun showComplicationConfig(
complicationId: Int
- ) = editorSession.launchComplicationProviderChooser(complicationId)
+ ) = editorSession.openComplicationProviderChooser(complicationId)
}
)
}
@@ -175,7 +174,7 @@
if (hasBackgroundComplication) {
topLevelOptionCount++
}
- val numComplications = editorSession.complicationState.size
+ val numComplications = editorSession.complicationsState.size
val hasNonBackgroundComplication =
numComplications > (if (hasBackgroundComplication) 1 else 0)
if (hasNonBackgroundComplication) {
@@ -189,7 +188,7 @@
// For a single complication go directly to the provider selector.
numComplications == 1 -> {
- val onlyComplication = editorSession.complicationState.entries.first()
+ val onlyComplication = editorSession.complicationsState.entries.first()
coroutineScope.launch {
fragmentController.showComplicationConfig(onlyComplication.key)
}
@@ -203,7 +202,7 @@
// There should only be a single userStyle setting if we get here.
val onlyStyleSetting = editorSession.userStyleSchema.userStyleSettings.first()
fragmentController.showStyleConfigFragment(
- onlyStyleSetting.id,
+ onlyStyleSetting.id.value,
editorSession.userStyleSchema,
editorSession.userStyle
)
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
index 22cc43d..d2c8270 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditingSessionTest.kt
@@ -51,15 +51,18 @@
import androidx.wear.watchface.MutableWatchState
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.client.asApiEditorState
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.editor.data.EditorStateWireFormat
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
+import androidx.wear.watchface.style.UserStyleSetting.Option
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -116,8 +119,8 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- val deferredEditorSession =
- EditorSession.createOnWatchEditingSessionAsyncImpl(
+ immediateCoroutineScope.launch {
+ editorSession = EditorSession.createOnWatchEditingSessionImpl(
this@OnWatchFaceEditingTestActivity,
intent!!,
object : ProviderInfoRetrieverProvider {
@@ -129,14 +132,14 @@
"ProviderApp1",
"Provider1",
providerIcon1,
- ComplicationType.SHORT_TEXT.asWireComplicationType(),
+ ComplicationType.SHORT_TEXT.toWireComplicationType(),
provider1
),
RIGHT_COMPLICATION_ID to ComplicationProviderInfo(
"ProviderApp2",
"Provider2",
providerIcon2,
- ComplicationType.LONG_TEXT.asWireComplicationType(),
+ ComplicationType.LONG_TEXT.toWireComplicationType(),
provider2
)
),
@@ -157,10 +160,7 @@
)
)
}
- )
-
- immediateCoroutineScope.launch {
- editorSession = deferredEditorSession.await()!!
+ )!!
}
}
}
@@ -219,7 +219,7 @@
Icon.createWithBitmap(
Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)
),
- ComplicationType.LONG_TEXT.asWireComplicationType(),
+ ComplicationType.LONG_TEXT.toWireComplicationType(),
provider3
)
)
@@ -229,59 +229,48 @@
}
}
-// Disables the requirement that watchFaceInstanceId has to be non-null on R and above.
-public class WatchFaceEditorContractForTest : WatchFaceEditorContract() {
- override fun nullWatchFaceInstanceIdOK() = true
-}
-
@RunWith(AndroidJUnit4::class)
@MediumTest
public class EditorSessionTest {
private val testComponentName = ComponentName("test.package", "test.class")
private val testEditorPackageName = "test.package"
- private val testInstanceId = "TEST_INSTANCE_ID"
+ private val testInstanceId = WatchFaceId("TEST_INSTANCE_ID")
private lateinit var editorDelegate: WatchFace.EditorDelegate
private val screenBounds = Rect(0, 0, 400, 400)
- private val redStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+ private val redStyleOption = ListOption(Option.Id("red_style"), "Red", icon = null)
- private val greenStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+ private val greenStyleOption = ListOption(Option.Id("green_style"), "Green", icon = null)
- private val blueStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+ private val blueStyleOption = ListOption(Option.Id("bluestyle"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
private val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
- private val classicStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+ private val classicStyleOption = ListOption(Option.Id("classic_style"), "Classic", icon = null)
- private val modernStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+ private val modernStyleOption = ListOption(Option.Id("modern_style"), "Modern", icon = null)
- private val gothicStyleOption =
- UserStyleSetting.ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+ private val gothicStyleOption = ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
private val watchHandStyleSetting = UserStyleSetting.ListUserStyleSetting(
- "hand_style_setting",
+ UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
private val placeholderWatchState = MutableWatchState().asWatchState()
@@ -361,10 +350,10 @@
private fun createOnWatchFaceEditingTestActivity(
userStyleSettings: List<UserStyleSetting>,
complications: List<Complication>,
- instanceId: String? = testInstanceId,
+ watchFaceId: WatchFaceId = testInstanceId,
previewReferenceTimeMillis: Long = 12345
): ActivityScenario<OnWatchFaceEditingTestActivity> {
- val userStyleRepository = UserStyleRepository(UserStyleSchema(userStyleSettings))
+ val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(userStyleSettings))
val complicationsManager = ComplicationsManager(complications, userStyleRepository)
// Mocking getters and setters with mockito at the same time is hard so we do this instead.
@@ -372,13 +361,15 @@
override val userStyleSchema = userStyleRepository.schema
override var userStyle: UserStyle
get() = userStyleRepository.userStyle
- set(value) { userStyleRepository.userStyle = value }
+ set(value) {
+ userStyleRepository.userStyle = value
+ }
override val complicationsManager = complicationsManager
override val screenBounds = [email protected]
override val previewReferenceTimeMillis = previewReferenceTimeMillis
- override fun takeScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, androidx.wear.complications.data.ComplicationData>?
@@ -391,9 +382,9 @@
WatchFace.registerEditorDelegate(testComponentName, editorDelegate)
return ActivityScenario.launch(
- WatchFaceEditorContractForTest().createIntent(
+ WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
- EditorRequest(testComponentName, testEditorPackageName, instanceId, null)
+ EditorRequest(testComponentName, testEditorPackageName, null, watchFaceId)
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
@@ -415,7 +406,7 @@
public fun instanceId() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
- assertThat(it.editorSession.instanceId).isEqualTo(testInstanceId)
+ assertThat(it.editorSession.watchFaceId.id).isEqualTo(testInstanceId.id)
}
}
@@ -448,10 +439,12 @@
scenario.onActivity {
val userStyleSchema = it.editorSession.userStyleSchema
assertThat(userStyleSchema.userStyleSettings.size).isEqualTo(2)
- assertThat(userStyleSchema.userStyleSettings[0].id).isEqualTo(colorStyleSetting.id)
+ assertThat(userStyleSchema.userStyleSettings[0].id.value)
+ .isEqualTo(colorStyleSetting.id.value)
assertThat(userStyleSchema.userStyleSettings[0].options.size)
.isEqualTo(colorStyleSetting.options.size)
- assertThat(userStyleSchema.userStyleSettings[1].id).isEqualTo(watchHandStyleSetting.id)
+ assertThat(userStyleSchema.userStyleSettings[1].id.value)
+ .isEqualTo(watchHandStyleSetting.id.value)
assertThat(userStyleSchema.userStyleSettings[1].options.size)
.isEqualTo(watchHandStyleSetting.options.size)
// We could test more state but this should be enough.
@@ -478,40 +471,42 @@
listOf(leftComplication, rightComplication, backgroundComplication)
)
scenario.onActivity {
- assertThat(it.editorSession.complicationState.size).isEqualTo(3)
- assertThat(it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.bounds)
+ assertThat(it.editorSession.complicationsState.size).isEqualTo(3)
+ assertThat(it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.bounds)
.isEqualTo(Rect(80, 160, 160, 240))
- assertThat(it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.boundsType)
+ assertThat(it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.boundsType)
.isEqualTo(ComplicationBoundsType.ROUND_RECT)
assertFalse(
- it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.fixedComplicationProvider
+ it.editorSession.complicationsState[
+ LEFT_COMPLICATION_ID
+ ]!!.fixedComplicationProvider
)
assertTrue(
- it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.isInitiallyEnabled
+ it.editorSession.complicationsState[LEFT_COMPLICATION_ID]!!.isInitiallyEnabled
)
- assertThat(it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.bounds)
+ assertThat(it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.bounds)
.isEqualTo(Rect(240, 160, 320, 240))
- assertThat(it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.boundsType)
+ assertThat(it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.boundsType)
.isEqualTo(ComplicationBoundsType.ROUND_RECT)
assertFalse(
- it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!
+ it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!
.fixedComplicationProvider
)
assertTrue(
- it.editorSession.complicationState[RIGHT_COMPLICATION_ID]!!.isInitiallyEnabled
+ it.editorSession.complicationsState[RIGHT_COMPLICATION_ID]!!.isInitiallyEnabled
)
- assertThat(it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.bounds)
+ assertThat(it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.bounds)
.isEqualTo(screenBounds)
- assertThat(it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.boundsType)
+ assertThat(it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.boundsType)
.isEqualTo(ComplicationBoundsType.BACKGROUND)
assertFalse(
- it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!
+ it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!
.fixedComplicationProvider
)
assertFalse(
- it.editorSession.complicationState[BACKGROUND_COMPLICATION_ID]!!.isInitiallyEnabled
+ it.editorSession.complicationsState[BACKGROUND_COMPLICATION_ID]!!.isInitiallyEnabled
)
// We could test more state but this should be enough.
}
@@ -544,15 +539,17 @@
)
scenario.onActivity {
assertTrue(
- it.editorSession.complicationState[LEFT_COMPLICATION_ID]!!.fixedComplicationProvider
+ it.editorSession.complicationsState[
+ LEFT_COMPLICATION_ID
+ ]!!.fixedComplicationProvider
)
try {
runBlocking {
- it.editorSession.launchComplicationProviderChooser(LEFT_COMPLICATION_ID)
+ it.editorSession.openComplicationProviderChooser(LEFT_COMPLICATION_ID)
fail(
- "launchComplicationProviderChooser should fail for a fixed complication " +
+ "openComplicationProviderChooser should fail for a fixed complication " +
"provider"
)
}
@@ -753,11 +750,11 @@
* Invoke [TestComplicationHelperActivity] which will change the provider (and hence
* the preview data) for [LEFT_COMPLICATION_ID].
*/
- assertTrue(editorSession.launchComplicationProviderChooser(LEFT_COMPLICATION_ID))
+ assertTrue(editorSession.openComplicationProviderChooser(LEFT_COMPLICATION_ID))
// This should update the preview data to point to the updated provider3 data.
val previewComplication =
- editorSession.getComplicationPreviewData()[LEFT_COMPLICATION_ID]
+ editorSession.getComplicationsPreviewData()[LEFT_COMPLICATION_ID]
as LongTextComplicationData
assertThat(
@@ -771,7 +768,7 @@
TestComplicationHelperActivity.lastIntent?.extras?.getString(
ProviderChooserIntent.EXTRA_WATCHFACE_INSTANCE_ID
)
- ).isEqualTo(testInstanceId)
+ ).isEqualTo(testInstanceId.id)
}
}
@@ -790,7 +787,7 @@
}
runBlocking {
- assertTrue(editorSession.launchComplicationProviderChooser(RIGHT_COMPLICATION_ID))
+ assertTrue(editorSession.openComplicationProviderChooser(RIGHT_COMPLICATION_ID))
assertThat(
TestComplicationHelperActivity.lastIntent?.extras?.getString(
@@ -816,12 +813,12 @@
}
@Test
- public fun takeWatchFaceScreenshot() {
+ public fun renderWatchFaceToBitmap() {
val scenario = createOnWatchFaceEditingTestActivity(emptyList(), emptyList())
scenario.onActivity {
assertThat(
- it.editorSession.takeWatchFaceScreenshot(
+ it.editorSession.renderWatchFaceToBitmap(
RenderParameters.DEFAULT_INTERACTIVE,
1234L,
null
@@ -858,18 +855,21 @@
TimeUnit.MILLISECONDS
).asApiEditorState()
- assertThat(result.userStyle[colorStyleSetting.id]).isEqualTo(blueStyleOption.id)
- assertThat(result.userStyle[watchHandStyleSetting.id]).isEqualTo(gothicStyleOption.id)
- assertThat(result.watchFaceInstanceId).isEqualTo(testInstanceId)
- assertTrue(result.commitChanges)
+ assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
+ .isEqualTo(blueStyleOption.id.value)
+ assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
+ .isEqualTo(gothicStyleOption.id.value)
+ assertThat(result.watchFaceId.id).isEqualTo(testInstanceId.id)
+ assertTrue(result.shouldCommitChanges)
// The style change should also have been applied to the watchface
- assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(blueStyleOption.id)
- assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
- .isEqualTo(gothicStyleOption.id)
+ assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+ .isEqualTo(blueStyleOption.id.value)
+ assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+ .isEqualTo(gothicStyleOption.id.value)
- assertThat(result.previewComplicationData.size).isEqualTo(2)
- val leftComplicationData = result.previewComplicationData[LEFT_COMPLICATION_ID] as
+ assertThat(result.previewComplicationsData.size).isEqualTo(2)
+ val leftComplicationData = result.previewComplicationsData[LEFT_COMPLICATION_ID] as
ShortTextComplicationData
assertThat(
leftComplicationData.text.getTextAt(
@@ -878,7 +878,7 @@
)
).isEqualTo("Left")
- val rightComplicationData = result.previewComplicationData[RIGHT_COMPLICATION_ID] as
+ val rightComplicationData = result.previewComplicationsData[RIGHT_COMPLICATION_ID] as
LongTextComplicationData
assertThat(
rightComplicationData.text.getTextAt(
@@ -891,11 +891,11 @@
}
@Test
- public fun nullInstanceId() {
+ public fun emptyInstanceId() {
val scenario = createOnWatchFaceEditingTestActivity(
listOf(colorStyleSetting, watchHandStyleSetting),
emptyList(),
- instanceId = null
+ watchFaceId = WatchFaceId("")
)
val editorObserver = TestEditorObserver()
@@ -903,7 +903,7 @@
scenario.onActivity { activity ->
runBlocking {
- assertThat(activity.editorSession.instanceId).isNull()
+ assertThat(activity.editorSession.watchFaceId.id).isEmpty()
activity.editorSession.close()
activity.finish()
}
@@ -913,7 +913,7 @@
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
- assertThat(result.watchFaceInstanceId).isEmpty()
+ assertThat(result.watchFaceId.id).isEmpty()
EditorService.globalEditorService.unregisterObserver(observerId)
}
@@ -934,7 +934,7 @@
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
- assertThat(result.previewComplicationData).isEmpty()
+ assertThat(result.previewComplicationsData).isEmpty()
EditorService.globalEditorService.unregisterObserver(observerId)
}
@@ -949,10 +949,10 @@
val observerId = EditorService.globalEditorService.registerObserver(editorObserver)
scenario.onActivity { activity ->
runBlocking {
- assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id)
- .isEqualTo(redStyleOption.id)
- assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
- .isEqualTo(classicStyleOption.id)
+ assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+ .isEqualTo(redStyleOption.id.value)
+ assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+ .isEqualTo(classicStyleOption.id.value)
// Select [blueStyleOption] and [gothicStyleOption].
val styleMap = activity.editorSession.userStyle.selectedOptions.toMutableMap()
@@ -972,15 +972,18 @@
TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS
).asApiEditorState()
- assertThat(result.userStyle[colorStyleSetting.id]).isEqualTo(blueStyleOption.id)
- assertThat(result.userStyle[watchHandStyleSetting.id]).isEqualTo(gothicStyleOption.id)
- assertFalse(result.commitChanges)
+ assertThat(result.userStyle.userStyleMap[colorStyleSetting.id.value])
+ .isEqualTo(blueStyleOption.id.value)
+ assertThat(result.userStyle.userStyleMap[watchHandStyleSetting.id.value])
+ .isEqualTo(gothicStyleOption.id.value)
+ assertFalse(result.shouldCommitChanges)
// The original style should be applied to the watch face however because
// commitChangesOnClose is false.
- assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(redStyleOption.id)
- assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
- .isEqualTo(classicStyleOption.id)
+ assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+ .isEqualTo(redStyleOption.id.value)
+ assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+ .isEqualTo(classicStyleOption.id.value)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@@ -990,7 +993,7 @@
runBlocking {
val intent = WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
- EditorRequest(testComponentName, testEditorPackageName, testInstanceId, null)
+ EditorRequest(testComponentName, testEditorPackageName, null, testInstanceId)
)
assertThat(intent.getPackage()).isEqualTo(testEditorPackageName)
@@ -998,7 +1001,7 @@
assertThat(editorRequest.editorPackageName).isEqualTo(testEditorPackageName)
assertThat(editorRequest.initialUserStyle).isNull()
assertThat(editorRequest.watchFaceComponentName).isEqualTo(testComponentName)
- assertThat(editorRequest.watchFaceInstanceId).isEqualTo(testInstanceId)
+ assertThat(editorRequest.watchFaceId.id).isEqualTo(testInstanceId.id)
}
}
@@ -1030,9 +1033,10 @@
assertFalse(editorObserver.stateChangeObserved())
// The style change should not have been applied to the watchface.
- assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id).isEqualTo(redStyleOption.id)
- assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id)
- .isEqualTo(classicStyleOption.id)
+ assertThat(editorDelegate.userStyle[colorStyleSetting]!!.id.value)
+ .isEqualTo(redStyleOption.id.value)
+ assertThat(editorDelegate.userStyle[watchHandStyleSetting]!!.id.value)
+ .isEqualTo(classicStyleOption.id.value)
EditorService.globalEditorService.unregisterObserver(observerId)
}
@@ -1040,9 +1044,14 @@
@Test
public fun closeEditorSessionBeforeWatchFaceDelegateCreated() {
val session: ActivityScenario<OnWatchFaceEditingTestActivity> = ActivityScenario.launch(
- WatchFaceEditorContractForTest().createIntent(
+ WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
- EditorRequest(testComponentName, testEditorPackageName, "instanceId", null)
+ EditorRequest(
+ testComponentName,
+ testEditorPackageName,
+ null,
+ WatchFaceId("instanceId")
+ )
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
diff --git a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
index 4855834..d867db8 100644
--- a/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
+++ b/wear/wear-watchface-editor/src/androidTest/java/androidx/wear/watchface/editor/EditorSessionGuavaTest.kt
@@ -35,8 +35,9 @@
import androidx.wear.watchface.ComplicationsManager
import androidx.wear.watchface.MutableWatchState
import androidx.wear.watchface.WatchFace
+import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
import com.google.common.truth.Truth
@@ -54,7 +55,7 @@
public class EditorSessionGuavaTest {
private val testComponentName = ComponentName("test.package", "test.class")
private val testEditorPackageName = "test.package"
- private val testInstanceId = "TEST_INSTANCE_ID"
+ private val testInstanceId = WatchFaceId("TEST_INSTANCE_ID")
private var editorDelegate = Mockito.mock(WatchFace.EditorDelegate::class.java)
private val screenBounds = Rect(0, 0, 400, 400)
@@ -98,10 +99,10 @@
private fun createOnWatchFaceEditingTestActivity(
userStyleSettings: List<UserStyleSetting>,
complications: List<Complication>,
- instanceId: String? = testInstanceId,
+ watchFaceId: WatchFaceId = testInstanceId,
previewReferenceTimeMillis: Long = 12345
): ActivityScenario<OnWatchFaceEditingTestActivity> {
- val userStyleRepository = UserStyleRepository(UserStyleSchema(userStyleSettings))
+ val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(userStyleSettings))
val complicationsManager = ComplicationsManager(complications, userStyleRepository)
WatchFace.registerEditorDelegate(testComponentName, editorDelegate)
@@ -113,9 +114,9 @@
.thenReturn(previewReferenceTimeMillis)
return ActivityScenario.launch(
- WatchFaceEditorContractForTest().createIntent(
+ WatchFaceEditorContract().createIntent(
ApplicationProvider.getApplicationContext<Context>(),
- EditorRequest(testComponentName, testEditorPackageName, instanceId, null)
+ EditorRequest(testComponentName, testEditorPackageName, null, watchFaceId)
).apply {
component = ComponentName(
ApplicationProvider.getApplicationContext<Context>(),
@@ -155,7 +156,7 @@
}
@Test
- public fun listenableLaunchComplicationProviderChooser() {
+ public fun listenableOpenComplicationProviderChooser() {
ComplicationProviderChooserContract.useTestComplicationHelperActivity = true
val scenario = createOnWatchFaceEditingTestActivity(
emptyList(),
@@ -172,7 +173,7 @@
* the preview data) for [LEFT_COMPLICATION_ID].
*/
assertTrue(
- listenableEditorSession.listenableLaunchComplicationProviderChooser(
+ listenableEditorSession.listenableOpenComplicationProviderChooser(
LEFT_COMPLICATION_ID
).get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
)
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
index 3f7b0d6..65863bd 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/EditorSession.kt
@@ -21,6 +21,7 @@
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
+import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -45,27 +46,29 @@
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.client.ComplicationState
-import androidx.wear.watchface.client.EditorObserverListener
+import androidx.wear.watchface.client.EditorListener
import androidx.wear.watchface.client.EditorServiceClient
import androidx.wear.watchface.client.EditorState
import androidx.wear.watchface.client.HeadlessWatchFaceClient
+import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.editor.data.EditorStateWireFormat
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleData
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.android.asCoroutineDispatcher
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Interface for manipulating watch face state during an editing session for a watch face editing
- * session. The editor should adjust [userStyle] and call [launchComplicationProviderChooser] to
+ * session. The editor should adjust [userStyle] and call [openComplicationProviderChooser] to
* configure the watch face and call [close] when done. This reports the updated [EditorState] to
- * the [EditorObserverListener]s registered via [EditorServiceClient.registerObserver].
+ * the [EditorListener]s registered via [EditorServiceClient.addListener].
*/
public abstract class EditorSession : AutoCloseable {
/** The [ComponentName] of the watch face being edited. */
@@ -76,7 +79,8 @@
* beyond, it's `null` on Android P and earlier. Note each distinct [ComponentName] can have
* multiple instances.
*/
- public abstract val instanceId: String?
+ @get:RequiresApi(Build.VERSION_CODES.R)
+ public abstract val watchFaceId: WatchFaceId
/** The current [UserStyle]. Assigning to this will cause the style to update. */
public abstract var userStyle: UserStyle
@@ -91,7 +95,7 @@
* Map of complication ids to [ComplicationState] for each complication slot. Note
* [ComplicationState] can change, typically in response to styling.
*/
- public abstract val complicationState: Map<Int, ComplicationState>
+ public abstract val complicationsState: Map<Int, ComplicationState>
/**
* Whether any changes should be committed when the session is closed (defaults to `true`).
@@ -114,11 +118,11 @@
* Returns a map of complication ids to preview [ComplicationData] suitable for use in rendering
* the watch face. Note if a slot is configured to be empty then it will not appear in the map,
* however disabled complications are included. Note also unlike live data this is static per
- * provider, but it may change (on the UIThread) as a result of
- * [launchComplicationProviderChooser].
+ * provider, but it may update (on the UiThread) as a result of
+ * [openComplicationProviderChooser].
*/
@UiThread
- public abstract suspend fun getComplicationPreviewData(): Map<Int, ComplicationData>
+ public abstract suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData>
/** The ID of the background complication or `null` if there isn't one. */
@get:SuppressWarnings("AutoBoxing")
@@ -130,25 +134,26 @@
public abstract fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int?
/**
- * Takes a screen shot of the watch face using the current [userStyle].
+ * Renders the watch face to a [Bitmap] using the current [userStyle].
*
* @param renderParameters The [RenderParameters] to render with
* @param calendarTimeMillis The UTC time in milliseconds since the epoch to render with
* @param idToComplicationData The [ComplicationData] for each complication to render with
*/
@UiThread
- public abstract fun takeWatchFaceScreenshot(
+ public abstract fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap
/**
- * Launches the complication provider chooser and returns `true` if the user made a selection or
- * `false` if the activity was canceled.
+ * Opens the complication provider chooser and returns `true` if the user made a selection or
+ * `false` if the activity was canceled. If the complication provider was changed then the map
+ * returned by [getComplicationsPreviewData] is updated (on the UiThread).
*/
@UiThread
- public abstract suspend fun launchComplicationProviderChooser(complicationId: Int): Boolean
+ public abstract suspend fun openComplicationProviderChooser(complicationId: Int): Boolean
public companion object {
/**
@@ -164,10 +169,10 @@
@SuppressWarnings("ExecutorRegistration")
@JvmStatic
@UiThread
- public fun createOnWatchEditingSessionAsync(
+ public suspend fun createOnWatchEditingSession(
activity: ComponentActivity,
editIntent: Intent
- ): Deferred<EditorSession?> = createOnWatchEditingSessionAsyncImpl(
+ ): EditorSession? = createOnWatchEditingSessionImpl(
activity,
editIntent,
object : ProviderInfoRetrieverProvider {
@@ -176,11 +181,11 @@
)
// Used by tests.
- internal fun createOnWatchEditingSessionAsyncImpl(
+ internal suspend fun createOnWatchEditingSessionImpl(
activity: ComponentActivity,
editIntent: Intent,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider
- ): Deferred<EditorSession?> = TraceEvent(
+ ): EditorSession? = TraceEvent(
"EditorSession.createOnWatchEditingSessionAsyncImpl"
).use {
val coroutineScope =
@@ -190,7 +195,7 @@
val session = OnWatchFaceEditorSessionImpl(
activity,
editorRequest.watchFaceComponentName,
- editorRequest.watchFaceInstanceId,
+ editorRequest.watchFaceId,
editorRequest.initialUserStyle,
providerInfoRetrieverProvider,
coroutineScope
@@ -198,17 +203,18 @@
// But full initialization has to be deferred because
// [WatchFace.getOrCreateEditorDelegate] is async.
- coroutineScope.async {
+ // Resolve only after init has been completed.
+ withContext(coroutineScope.coroutineContext) {
session.setEditorDelegate(
WatchFace.getOrCreateEditorDelegate(
editorRequest.watchFaceComponentName
).await()!!
)
- // Resolve the Deferred<EditorSession?> only after init has been completed.
+ // Resolve only after init has been completed.
session
}
- } ?: CompletableDeferred(null)
+ }
}
/**
@@ -232,7 +238,7 @@
activity,
headlessWatchFaceClient,
it.watchFaceComponentName,
- it.watchFaceInstanceId,
+ it.watchFaceId,
it.initialUserStyle!!,
object : ProviderInfoRetrieverProvider {
override fun getProviderInfoRetriever() = ProviderInfoRetriever(activity)
@@ -279,7 +285,7 @@
private val deferredComplicationPreviewDataMap =
CompletableDeferred<MutableMap<Int, ComplicationData>>()
- override suspend fun getComplicationPreviewData(): Map<Int, ComplicationData> {
+ override suspend fun getComplicationsPreviewData(): Map<Int, ComplicationData> {
return deferredComplicationPreviewDataMap.await()
}
@@ -313,33 +319,33 @@
}
}
- override suspend fun launchComplicationProviderChooser(
+ override suspend fun openComplicationProviderChooser(
complicationId: Int
): Boolean = TraceEvent(
"BaseEditorSession.launchComplicationProviderChooser $complicationId"
).use {
requireNotClosed()
- require(!complicationState[complicationId]!!.fixedComplicationProvider) {
+ require(!complicationsState[complicationId]!!.fixedComplicationProvider) {
"Can't configure fixed complication ID $complicationId"
}
pendingComplicationProviderChooserResult = CompletableDeferred<Boolean>()
pendingComplicationProviderId = complicationId
chooseComplicationProvider.launch(
- ComplicationProviderChooserRequest(this, complicationId, instanceId)
+ ComplicationProviderChooserRequest(this, complicationId, watchFaceId.id)
)
return pendingComplicationProviderChooserResult!!.await()
}
override val backgroundComplicationId: Int? by lazy {
requireNotClosed()
- complicationState.entries.firstOrNull {
+ complicationsState.entries.firstOrNull {
it.value.boundsType == ComplicationBoundsType.BACKGROUND
}?.key
}
override fun getComplicationIdAt(@Px x: Int, @Px y: Int): Int? {
requireNotClosed()
- return complicationState.entries.firstOrNull {
+ return complicationsState.entries.firstOrNull {
it.value.isEnabled && when (it.value.boundsType) {
ComplicationBoundsType.ROUND_RECT -> it.value.bounds.contains(x, y)
ComplicationBoundsType.BACKGROUND -> false
@@ -405,7 +411,7 @@
// better to crash and start over.
val providerInfoArray = providerInfoRetriever.retrieveProviderInfo(
watchFaceComponentName,
- complicationState.keys.toIntArray()
+ complicationsState.keys.toIntArray()
)
deferredComplicationPreviewDataMap.complete(
// Parallel fetch preview ComplicationData.
@@ -433,9 +439,9 @@
EditorService.globalEditorService.removeCloseCallback(closeCallback)
coroutineScope.launchWithTracing("BaseEditorSession.close") {
val editorState = EditorStateWireFormat(
- instanceId,
+ watchFaceId.id,
userStyle.toWireFormat(),
- getComplicationPreviewData().map {
+ getComplicationsPreviewData().map {
IdAndComplicationDataWireFormat(
it.key,
it.value.asWireComplicationData()
@@ -474,8 +480,8 @@
internal class OnWatchFaceEditorSessionImpl(
activity: ComponentActivity,
override val watchFaceComponentName: ComponentName,
- override val instanceId: String?,
- private val initialEditorUserStyle: Map<String, String>?,
+ override val watchFaceId: WatchFaceId,
+ private val initialEditorUserStyle: UserStyleData?,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
coroutineScope: CoroutineScope
) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
@@ -488,7 +494,7 @@
override val previewReferenceTimeMillis by lazy { editorDelegate.previewReferenceTimeMillis }
- override val complicationState
+ override val complicationsState
get() = editorDelegate.complicationsManager.complications.mapValues {
requireNotClosed()
ComplicationState(
@@ -525,13 +531,13 @@
private lateinit var previousWatchFaceUserStyle: UserStyle
- override fun takeWatchFaceScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap {
requireNotClosed()
- return editorDelegate.takeScreenshot(
+ return editorDelegate.renderWatchFaceToBitmap(
renderParameters,
calendarTimeMillis,
idToComplicationData
@@ -569,8 +575,8 @@
activity: ComponentActivity,
private val headlessWatchFaceClient: HeadlessWatchFaceClient,
override val watchFaceComponentName: ComponentName,
- override val instanceId: String?,
- initialUserStyle: Map<String, String>,
+ override val watchFaceId: WatchFaceId,
+ initialUserStyle: UserStyleData,
providerInfoRetrieverProvider: ProviderInfoRetrieverProvider,
coroutineScope: CoroutineScope,
) : BaseEditorSession(activity, providerInfoRetrieverProvider, coroutineScope) {
@@ -580,15 +586,15 @@
override val previewReferenceTimeMillis = headlessWatchFaceClient.previewReferenceTimeMillis
- override val complicationState = headlessWatchFaceClient.complicationState
+ override val complicationsState = headlessWatchFaceClient.complicationsState
- override fun takeWatchFaceScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
): Bitmap {
requireNotClosed()
- return headlessWatchFaceClient.takeWatchFaceScreenshot(
+ return headlessWatchFaceClient.renderWatchFaceToBitmap(
renderParameters,
calendarTimeMillis,
userStyle,
@@ -630,10 +636,10 @@
context,
input.editorSession.watchFaceComponentName,
input.complicationId,
- input.editorSession.complicationState[input.complicationId]!!.supportedTypes,
+ input.editorSession.complicationsState[input.complicationId]!!.supportedTypes,
input.instanceId
)
- val complicationState = input.editorSession.complicationState[input.complicationId]!!
+ val complicationState = input.editorSession.complicationsState[input.complicationId]!!
intent.replaceExtras(
Bundle(complicationState.complicationConfigExtras).apply { putAll(intent.extras!!) }
)
diff --git a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
index 66d94c9..f09e23c 100644
--- a/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
+++ b/wear/wear-watchface-editor/src/main/java/androidx/wear/watchface/editor/WatchFaceEditorContract.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.editor
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Context
@@ -23,10 +24,13 @@
import android.os.Build
import android.support.wearable.watchface.Constants
import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RequiresApi
import androidx.wear.watchface.client.EditorServiceClient
import androidx.wear.watchface.client.EditorState
import androidx.wear.watchface.client.WatchFaceControlClient
+import androidx.wear.watchface.client.WatchFaceId
import androidx.wear.watchface.style.UserStyle
+import androidx.wear.watchface.style.UserStyleData
internal const val INSTANCE_ID_KEY: String = "INSTANCE_ID_KEY"
internal const val COMPONENT_NAME_KEY: String = "COMPONENT_NAME_KEY"
@@ -38,48 +42,75 @@
*
* @param watchFaceComponentName The [ComponentName] of the watch face being edited.
* @param editorPackageName The package name of the watch face editor APK.
- * @param watchFaceInstanceId Unique ID for the instance of the watch face being edited, only
- * defined for Android R and beyond, it's `null` on Android P and earlier. Note each distinct
+ * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
+ * required for a headless [EditorSession].
+ * @param watchFaceId Unique ID for the instance of the watch face being edited, only
+ * defined for Android R and beyond, it's `null` on Android P and earlier. Note each distinct
* [ComponentName] can have multiple instances.
- * @param initialUserStyle The initial [UserStyle], only required for a headless [EditorSession].
*/
-public class EditorRequest(
+public class EditorRequest @RequiresApi(Build.VERSION_CODES.R) constructor(
public val watchFaceComponentName: ComponentName,
public val editorPackageName: String,
- public val watchFaceInstanceId: String?,
- public val initialUserStyle: Map<String, String>?
+ public val initialUserStyle: UserStyleData?,
+
+ @get:RequiresApi(Build.VERSION_CODES.R)
+ @RequiresApi(Build.VERSION_CODES.R)
+ public val watchFaceId: WatchFaceId
) {
+ /**
+ * Constructs an [EditorRequest] without a [WatchFaceId]. This is for use pre-android R.
+ *
+ * @param watchFaceComponentName The [ComponentName] of the watch face being edited.
+ * @param editorPackageName The package name of the watch face editor APK.
+ * @param initialUserStyle The initial [UserStyle] stored as a [UserStyleData] or `null`. Only
+ * required for a headless [EditorSession].
+ * [EditorSession].
+ */
+ @SuppressLint("NewApi")
+ public constructor(
+ watchFaceComponentName: ComponentName,
+ editorPackageName: String,
+ initialUserStyle: UserStyleData?
+ ) : this(
+ watchFaceComponentName,
+ editorPackageName,
+ initialUserStyle,
+ WatchFaceId("")
+ )
+
public companion object {
/**
* Returns an [EditorRequest] saved to a [Intent] by [WatchFaceEditorContract.createIntent]
* if there is one or `null` otherwise. Intended for use by the watch face editor activity.
*/
+ @SuppressLint("NewApi")
@JvmStatic
public fun createFromIntent(intent: Intent): EditorRequest? {
val componentName =
intent.getParcelableExtra<ComponentName>(COMPONENT_NAME_KEY)
?: intent.getParcelableExtra(Constants.EXTRA_WATCH_FACE_COMPONENT)
val editorPackageName = intent.getPackage() ?: ""
- val instanceId = intent.getStringExtra(INSTANCE_ID_KEY)
+ val instanceId = WatchFaceId(intent.getStringExtra(INSTANCE_ID_KEY) ?: "")
val userStyleKey = intent.getStringArrayExtra(USER_STYLE_KEY)
- val userStyleValue = intent.getStringArrayExtra(USER_STYLE_VALUES)
return componentName?.let {
- if (userStyleKey != null && userStyleValue != null &&
- userStyleKey.size == userStyleValue.size
- ) {
+ if (userStyleKey != null) {
EditorRequest(
componentName,
editorPackageName,
- instanceId,
- HashMap<String, String>().apply {
- for (i in userStyleKey.indices) {
- put(userStyleKey[i], userStyleValue[i])
+ UserStyleData(
+ HashMap<String, ByteArray>().apply {
+ for (i in userStyleKey.indices) {
+ val userStyleValue =
+ intent.getByteArrayExtra(USER_STYLE_VALUES + i)
+ ?: return null
+ put(userStyleKey[i], userStyleValue)
+ }
}
- }
+ ),
+ instanceId
)
- } else {
- EditorRequest(componentName, editorPackageName, instanceId, null)
}
+ EditorRequest(componentName, editorPackageName, null, instanceId)
}
}
}
@@ -90,7 +121,7 @@
* by SysUI and the normal activity result isn't used for returning [EditorState] because
* [Activity.onStop] isn't guaranteed to be called when SysUI UX needs it to. Instead [EditorState]
* is broadcast by the editor using[EditorSession.close], to observe these broadcasts use
- * [WatchFaceControlClient.getEditorServiceClient] and [EditorServiceClient.registerObserver].
+ * [WatchFaceControlClient.getEditorServiceClient] and [EditorServiceClient.addListener].
*/
public open class WatchFaceEditorContract : ActivityResultContract<EditorRequest, Unit>() {
@@ -99,26 +130,19 @@
"androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"
}
- // Required for testing.
- internal open fun nullWatchFaceInstanceIdOK() =
- Build.VERSION.SDK_INT < Build.VERSION_CODES.R
-
override fun createIntent(
context: Context,
input: EditorRequest
): Intent {
- require(
- input.watchFaceInstanceId != null || nullWatchFaceInstanceIdOK()
- ) {
- "watchFaceInstanceId must be set from Android R and above"
- }
return Intent(ACTION_WATCH_FACE_EDITOR).apply {
setPackage(input.editorPackageName)
putExtra(COMPONENT_NAME_KEY, input.watchFaceComponentName)
- putExtra(INSTANCE_ID_KEY, input.watchFaceInstanceId)
+ putExtra(INSTANCE_ID_KEY, input.watchFaceId.id)
input.initialUserStyle?.let {
- putExtra(USER_STYLE_KEY, it.keys.toTypedArray())
- putExtra(USER_STYLE_VALUES, it.values.toTypedArray())
+ putExtra(USER_STYLE_KEY, it.userStyleMap.keys.toTypedArray())
+ for ((index, value) in it.userStyleMap.values.withIndex()) {
+ putExtra(USER_STYLE_VALUES + index, value)
+ }
}
}
}
diff --git a/wear/wear-watchface-style/api/current.txt b/wear/wear-watchface-style/api/current.txt
index 93e065c..c63feab 100644
--- a/wear/wear-watchface-style/api/current.txt
+++ b/wear/wear-watchface-style/api/current.txt
@@ -1,37 +1,43 @@
// Signature format: 4.0
package androidx.wear.watchface.style {
- public enum Layer {
- enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
- enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
- enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
- }
-
- public final class UserStyle {
- ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
- ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
- method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
- method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
- method public java.util.Map<java.lang.String,java.lang.String> toMap();
- property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
- }
-
- public final class UserStyleRepository {
- ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
- method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ public final class CurrentUserStyleRepository {
+ ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+ method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method public androidx.wear.watchface.style.UserStyleSchema getSchema();
method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
- method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
property public final androidx.wear.watchface.style.UserStyleSchema schema;
property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
}
- public static interface UserStyleRepository.UserStyleListener {
+ public static interface CurrentUserStyleRepository.UserStyleChangeListener {
method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
}
+ public enum Layer {
+ enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+ }
+
+ public final class UserStyle {
+ ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+ method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+ method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+ method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+ property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+ }
+
+ public final class UserStyleData {
+ ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+ method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+ property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+ }
+
public final class UserStyleSchema {
ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getUserStyleSettings();
@@ -39,28 +45,27 @@
}
public abstract sealed class UserStyleSetting {
- method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+ method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
method public final int getDefaultOptionIndex();
method public final CharSequence getDescription();
method public final CharSequence getDisplayName();
method public final android.graphics.drawable.Icon? getIcon();
- method public final String getId();
- method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+ method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
- property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+ property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
property public final int defaultOptionIndex;
property public final CharSequence description;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
method public boolean getDefaultValue();
}
@@ -74,8 +79,8 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay {
@@ -96,7 +101,7 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
@@ -106,17 +111,17 @@
}
public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+ ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
}
public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
- method public String getCustomValue();
- property public final String customValue;
+ ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+ method public byte[] getCustomValue();
+ property public final byte[] customValue;
}
public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
method public double getDefaultValue();
method public double getMaximumValue();
method public double getMinimumValue();
@@ -128,13 +133,24 @@
property public final double value;
}
+ public static final class UserStyleSetting.Id {
+ ctor public UserStyleSetting.Id(String value);
+ method public String getValue();
+ property public final String value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 40; // 0x28
+ }
+
+ public static final class UserStyleSetting.Id.Companion {
+ }
+
public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
property public final CharSequence displayName;
@@ -142,7 +158,7 @@
}
public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
method public long getDefaultValue();
method public long getMaximumValue();
method public long getMinimumValue();
@@ -155,21 +171,32 @@
}
public abstract static class UserStyleSetting.Option {
- ctor public UserStyleSetting.Option(String id);
- method public final String getId();
+ ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.Option.Companion {
}
+ public static final class UserStyleSetting.Option.Id {
+ ctor public UserStyleSetting.Option.Id(byte[] value);
+ ctor public UserStyleSetting.Option.Id(String value);
+ method public byte[] getValue();
+ property public final byte[] value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 1024; // 0x400
+ }
+
+ public static final class UserStyleSetting.Option.Id.Companion {
+ }
+
}
diff --git a/wear/wear-watchface-style/api/public_plus_experimental_current.txt b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
index f92f80a..bd690a3 100644
--- a/wear/wear-watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-style/api/public_plus_experimental_current.txt
@@ -1,37 +1,43 @@
// Signature format: 4.0
package androidx.wear.watchface.style {
- public enum Layer {
- enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
- enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
- enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
- }
-
- public final class UserStyle {
- ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
- ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
- method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
- method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
- method public java.util.Map<java.lang.String,java.lang.String> toMap();
- property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
- }
-
- public final class UserStyleRepository {
- ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
- method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ public final class CurrentUserStyleRepository {
+ ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+ method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method public androidx.wear.watchface.style.UserStyleSchema getSchema();
method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
- method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
property public final androidx.wear.watchface.style.UserStyleSchema schema;
property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
}
- public static interface UserStyleRepository.UserStyleListener {
+ public static interface CurrentUserStyleRepository.UserStyleChangeListener {
method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
}
+ public enum Layer {
+ enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+ }
+
+ public final class UserStyle {
+ ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+ method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+ method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+ method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+ property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+ }
+
+ public final class UserStyleData {
+ ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+ method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+ property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+ }
+
public final class UserStyleSchema {
ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getUserStyleSettings();
@@ -39,28 +45,27 @@
}
public abstract sealed class UserStyleSetting {
- method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+ method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
method public final int getDefaultOptionIndex();
method public final CharSequence getDescription();
method public final CharSequence getDisplayName();
method public final android.graphics.drawable.Icon? getIcon();
- method public final String getId();
- method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+ method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
- property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+ property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
property public final int defaultOptionIndex;
property public final CharSequence description;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
method public boolean getDefaultValue();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat toWireFormat();
}
@@ -76,8 +81,8 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat toWireFormat();
}
@@ -99,7 +104,7 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
@@ -110,19 +115,19 @@
}
public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+ ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueUserStyleSettingWireFormat toWireFormat();
}
public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
- method public String getCustomValue();
+ ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+ method public byte[] getCustomValue();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueOptionWireFormat toWireFormat();
- property public final String customValue;
+ property public final byte[] customValue;
}
public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
method public double getDefaultValue();
method public double getMaximumValue();
method public double getMinimumValue();
@@ -136,14 +141,25 @@
property public final double value;
}
+ public static final class UserStyleSetting.Id {
+ ctor public UserStyleSetting.Id(String value);
+ method public String getValue();
+ property public final String value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 40; // 0x28
+ }
+
+ public static final class UserStyleSetting.Id.Companion {
+ }
+
public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListUserStyleSettingWireFormat toWireFormat();
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListOptionWireFormat toWireFormat();
@@ -152,7 +168,7 @@
}
public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
method public long getDefaultValue();
method public long getMaximumValue();
method public long getMinimumValue();
@@ -167,21 +183,32 @@
}
public abstract static class UserStyleSetting.Option {
- ctor public UserStyleSetting.Option(String id);
- method public final String getId();
+ ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
method public final androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption? toDoubleRangeOption();
method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.Option.Companion {
}
+ public static final class UserStyleSetting.Option.Id {
+ ctor public UserStyleSetting.Option.Id(byte[] value);
+ ctor public UserStyleSetting.Option.Id(String value);
+ method public byte[] getValue();
+ property public final byte[] value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 1024; // 0x400
+ }
+
+ public static final class UserStyleSetting.Option.Id.Companion {
+ }
+
}
diff --git a/wear/wear-watchface-style/api/restricted_current.txt b/wear/wear-watchface-style/api/restricted_current.txt
index a9db649..2b3bab3 100644
--- a/wear/wear-watchface-style/api/restricted_current.txt
+++ b/wear/wear-watchface-style/api/restricted_current.txt
@@ -1,39 +1,46 @@
// Signature format: 4.0
package androidx.wear.watchface.style {
- public enum Layer {
- enum_constant public static final androidx.wear.watchface.style.Layer BASE_LAYER;
- enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
- enum_constant public static final androidx.wear.watchface.style.Layer TOP_LAYER;
- }
-
- public final class UserStyle {
- ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
- ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
- ctor public UserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
- ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyle(androidx.wear.watchface.style.data.UserStyleWireFormat userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
- method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
- method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
- method public java.util.Map<java.lang.String,java.lang.String> toMap();
- method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
- property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
- }
-
- public final class UserStyleRepository {
- ctor public UserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
- method @UiThread public void addUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ public final class CurrentUserStyleRepository {
+ ctor public CurrentUserStyleRepository(androidx.wear.watchface.style.UserStyleSchema schema);
+ method @UiThread public void addUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method public androidx.wear.watchface.style.UserStyleSchema getSchema();
method @UiThread public androidx.wear.watchface.style.UserStyle getUserStyle();
- method @UiThread public void removeUserStyleListener(androidx.wear.watchface.style.UserStyleRepository.UserStyleListener userStyleListener);
+ method @UiThread public void removeUserStyleChangeListener(androidx.wear.watchface.style.CurrentUserStyleRepository.UserStyleChangeListener userStyleChangeListener);
method @UiThread public void setUserStyle(androidx.wear.watchface.style.UserStyle style);
property public final androidx.wear.watchface.style.UserStyleSchema schema;
property @UiThread public final androidx.wear.watchface.style.UserStyle userStyle;
}
- public static interface UserStyleRepository.UserStyleListener {
+ public static interface CurrentUserStyleRepository.UserStyleChangeListener {
method @UiThread public void onUserStyleChanged(androidx.wear.watchface.style.UserStyle userStyle);
}
+ public enum Layer {
+ enum_constant public static final androidx.wear.watchface.style.Layer BASE;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS;
+ enum_constant public static final androidx.wear.watchface.style.Layer COMPLICATIONS_OVERLAY;
+ }
+
+ public final class UserStyle {
+ ctor public UserStyle(java.util.Map<androidx.wear.watchface.style.UserStyleSetting,? extends androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyle userStyle);
+ ctor public UserStyle(androidx.wear.watchface.style.UserStyleData userStyle, androidx.wear.watchface.style.UserStyleSchema styleSchema);
+ method public operator androidx.wear.watchface.style.UserStyleSetting.Option? get(androidx.wear.watchface.style.UserStyleSetting setting);
+ method public java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> getSelectedOptions();
+ method public androidx.wear.watchface.style.UserStyleData toUserStyleData();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
+ property public final java.util.Map<androidx.wear.watchface.style.UserStyleSetting,androidx.wear.watchface.style.UserStyleSetting.Option> selectedOptions;
+ }
+
+ public final class UserStyleData {
+ ctor public UserStyleData(java.util.Map<java.lang.String,byte[]> userStyleMap);
+ ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyleData(androidx.wear.watchface.style.data.UserStyleWireFormat userStyle);
+ method public java.util.Map<java.lang.String,byte[]> getUserStyleMap();
+ method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.UserStyleWireFormat toWireFormat();
+ property public final java.util.Map<java.lang.String,byte[]> userStyleMap;
+ }
+
public final class UserStyleSchema {
ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
ctor @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public UserStyleSchema(androidx.wear.watchface.style.data.UserStyleSchemaWireFormat wireFormat);
@@ -43,30 +50,29 @@
}
public abstract sealed class UserStyleSetting {
- method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectsLayers();
+ method public final java.util.Collection<androidx.wear.watchface.style.Layer> getAffectedLayers();
method public final androidx.wear.watchface.style.UserStyleSetting.Option getDefaultOption();
method public final int getDefaultOptionIndex();
method public final CharSequence getDescription();
method public final CharSequence getDisplayName();
method public final android.graphics.drawable.Icon? getIcon();
- method public final String getId();
- method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(String optionId);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Id getId();
+ method public androidx.wear.watchface.style.UserStyleSetting.Option getOptionForId(byte[] optionId);
method public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> getOptions();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final java.util.List<androidx.wear.watchface.style.data.OptionWireFormat> getWireFormatOptionsList();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.UserStyleSettingWireFormat toWireFormat();
- property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectsLayers;
+ property public final java.util.Collection<androidx.wear.watchface.style.Layer> affectedLayers;
property public final int defaultOptionIndex;
property public final CharSequence description;
property public final CharSequence displayName;
property public final android.graphics.drawable.Icon? icon;
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Id id;
property public final java.util.List<androidx.wear.watchface.style.UserStyleSetting.Option> options;
field public static final androidx.wear.watchface.style.UserStyleSetting.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.BooleanUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.BooleanUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
+ ctor public UserStyleSetting.BooleanUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, boolean defaultValue);
method public boolean getDefaultValue();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat toWireFormat();
}
@@ -82,8 +88,8 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
- ctor public UserStyleSetting.ComplicationsUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption defaultOption);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption> complicationConfig, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ComplicationsUserStyleSettingWireFormat toWireFormat();
}
@@ -105,7 +111,7 @@
}
public static final class UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
+ ctor public UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon, java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> complicationOverlays);
method public java.util.Collection<androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay> getComplicationOverlays();
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
@@ -116,19 +122,19 @@
}
public static final class UserStyleSetting.CustomValueUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, String defaultValue);
+ ctor public UserStyleSetting.CustomValueUserStyleSetting(java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, byte[] defaultValue);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueUserStyleSettingWireFormat toWireFormat();
}
public static final class UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(String customValue);
- method public String getCustomValue();
+ ctor public UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption(byte[] customValue);
+ method public byte[] getCustomValue();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.CustomValueOptionWireFormat toWireFormat();
- property public final String customValue;
+ property public final byte[] customValue;
}
public static final class UserStyleSetting.DoubleRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.DoubleRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
+ ctor public UserStyleSetting.DoubleRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, double minimumValue, double maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, double defaultValue);
method public double getDefaultValue();
method public double getMaximumValue();
method public double getMinimumValue();
@@ -142,14 +148,25 @@
property public final double value;
}
+ public static final class UserStyleSetting.Id {
+ ctor public UserStyleSetting.Id(String value);
+ method public String getValue();
+ property public final String value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 40; // 0x28
+ }
+
+ public static final class UserStyleSetting.Id.Companion {
+ }
+
public static class UserStyleSetting.ListUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
- ctor public UserStyleSetting.ListUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, optional androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption defaultOption);
+ ctor public UserStyleSetting.ListUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, java.util.List<androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption> options, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListUserStyleSettingWireFormat toWireFormat();
}
public static final class UserStyleSetting.ListUserStyleSetting.ListOption extends androidx.wear.watchface.style.UserStyleSetting.Option {
- ctor public UserStyleSetting.ListUserStyleSetting.ListOption(String id, CharSequence displayName, android.graphics.drawable.Icon? icon);
+ ctor public UserStyleSetting.ListUserStyleSetting.ListOption(androidx.wear.watchface.style.UserStyleSetting.Option.Id id, CharSequence displayName, android.graphics.drawable.Icon? icon);
method public CharSequence getDisplayName();
method public android.graphics.drawable.Icon? getIcon();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.data.ListOptionWireFormat toWireFormat();
@@ -158,7 +175,7 @@
}
public static final class UserStyleSetting.LongRangeUserStyleSetting extends androidx.wear.watchface.style.UserStyleSetting {
- ctor public UserStyleSetting.LongRangeUserStyleSetting(String id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
+ ctor public UserStyleSetting.LongRangeUserStyleSetting(androidx.wear.watchface.style.UserStyleSetting.Id id, CharSequence displayName, CharSequence description, android.graphics.drawable.Icon? icon, long minimumValue, long maximumValue, java.util.Collection<? extends androidx.wear.watchface.style.Layer> affectsLayers, long defaultValue);
method public long getDefaultValue();
method public long getMaximumValue();
method public long getMinimumValue();
@@ -173,8 +190,8 @@
}
public abstract static class UserStyleSetting.Option {
- ctor public UserStyleSetting.Option(String id);
- method public final String getId();
+ ctor public UserStyleSetting.Option(androidx.wear.watchface.style.UserStyleSetting.Option.Id id);
+ method public final androidx.wear.watchface.style.UserStyleSetting.Option.Id getId();
method public final androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting.BooleanOption? toBooleanOption();
method public final androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption? toComplicationsOption();
method public final androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption? toCustomValueOption();
@@ -182,14 +199,25 @@
method public final androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption? toListOption();
method public final androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption? toLongRangeOption();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract androidx.wear.watchface.style.data.OptionWireFormat toWireFormat();
- property public final String id;
+ property public final androidx.wear.watchface.style.UserStyleSetting.Option.Id id;
field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Companion Companion;
- field public static final int maxIdLength;
}
public static final class UserStyleSetting.Option.Companion {
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public androidx.wear.watchface.style.UserStyleSetting.Option createFromWireFormat(androidx.wear.watchface.style.data.OptionWireFormat wireFormat);
}
+ public static final class UserStyleSetting.Option.Id {
+ ctor public UserStyleSetting.Option.Id(byte[] value);
+ ctor public UserStyleSetting.Option.Id(String value);
+ method public byte[] getValue();
+ property public final byte[] value;
+ field public static final androidx.wear.watchface.style.UserStyleSetting.Option.Id.Companion Companion;
+ field public static final int MAX_LENGTH = 1024; // 0x400
+ }
+
+ public static final class UserStyleSetting.Option.Id.Companion {
+ }
+
}
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
similarity index 64%
rename from wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
rename to wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index ba933ce7..99f5ee2 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -24,7 +24,9 @@
/**
* The users style choices represented as a map of [UserStyleSetting] to
- * [UserStyleSetting.Option].
+ * [UserStyleSetting.Option]. This is intended for use by the WatchFace and the [selectedOptions]
+ * map keys are the same objects as in the [UserStyleSchema]. This means you can't serialize a
+ * UserStyle directly, instead you need to use a [UserStyleData] (see [toUserStyleData]).
*
* @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
*/
@@ -39,22 +41,21 @@
public constructor(userStyle: UserStyle) : this(HashMap(userStyle.selectedOptions))
/**
- * Constructs a [UserStyle] from a Map<String, String> and the [UserStyleSchema]. Unrecognized
+ * Constructs a [UserStyle] from a [UserStyleData] and the [UserStyleSchema]. Unrecognized
* style settings will be ignored. Unlisted style settings will be initialized with that
* settings default option.
*
- * @param userStyle The [UserStyle] represented as a Map<String, String> of
- * [UserStyleSetting.id] to [UserStyleSetting.Option.id]
- * @param styleSchema The [UserStyleSchema] for this UserStyle, describes how we interpret
+ * @param userStyle The [UserStyle] represented as a [UserStyleData].
+ * @param styleSchema The for this UserStyle, describes how we interpret
* [userStyle].
*/
public constructor(
- userStyle: Map<String, String>,
+ userStyle: UserStyleData,
styleSchema: UserStyleSchema
) : this(
HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
for (styleSetting in styleSchema.userStyleSettings) {
- val option = userStyle[styleSetting.id]
+ val option = userStyle.userStyleMap[styleSetting.id.value]
if (option != null) {
this[styleSetting] = styleSetting.getSettingOptionForId(option)
} else {
@@ -66,19 +67,14 @@
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public constructor(
- userStyle: UserStyleWireFormat,
- styleSchema: UserStyleSchema
- ) : this(userStyle.mUserStyle, styleSchema)
+ public fun toWireFormat(): UserStyleWireFormat = UserStyleWireFormat(toMap())
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
- public fun toWireFormat(): UserStyleWireFormat =
- UserStyleWireFormat(toMap())
+ /** Returns the style as a [UserStyleData]. */
+ public fun toUserStyleData(): UserStyleData = UserStyleData(toMap())
- /** Returns the style as a Map<String, String>. */
- public fun toMap(): Map<String, String> =
- selectedOptions.entries.associate { it.key.id to it.value.id }
+ /** Returns the style as a [Map]<[String], [ByteArray]>. */
+ private fun toMap(): Map<String, ByteArray> =
+ selectedOptions.entries.associate { it.key.id.value to it.value.id.value }
/** Returns the [UserStyleSetting.Option] for [setting] if there is one or `null` otherwise. */
public operator fun get(setting: UserStyleSetting): UserStyleSetting.Option? =
@@ -86,11 +82,37 @@
override fun toString(): String =
"[" + selectedOptions.entries.joinToString(
- transform = { it.key.id + " -> " + it.value.id }
+ transform = { it.key.id.value + " -> " + it.value.id.value }
) + "]"
}
/**
+ * A form of [UserStyle] which is easy to serialize. This is intended for use by the watch face
+ * clients and the editor where we can't practically use [UserStyle] due to it's limitations.
+ */
+public class UserStyleData(
+ public val userStyleMap: Map<String, ByteArray>
+) {
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public constructor(
+ userStyle: UserStyleWireFormat
+ ) : this(userStyle.mUserStyle)
+
+ override fun toString(): String = "{" + userStyleMap.map {
+ try {
+ it.key + "=" + it.value.decodeToString()
+ } catch (e: Exception) {
+ it.key + "=" + it.value
+ }
+ }.joinToString() + "}"
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public fun toWireFormat(): UserStyleWireFormat = UserStyleWireFormat(userStyleMap)
+}
+
+/**
* Describes the list of [UserStyleSetting]s the user can configure.
*
* @param userStyleSettings The user configurable style categories associated with this watch
@@ -131,30 +153,30 @@
}
/**
- * An in memory storage for user style choices represented as [UserStyle], listeners can be
- * registered to observe style changes. The UserStyleRepository is initialized with a
+ * In memory storage for the current user style choices represented as [UserStyle], listeners can be
+ * registered to observe style changes. The CurrentUserStyleRepository is initialized with a
* [UserStyleSchema].
*
- * @param schema The [UserStyleSchema] for this UserStyleRepository which describes the available
- * style categories.
+ * @param schema The [UserStyleSchema] for this CurrentUserStyleRepository which describes the
+ * available style categories.
*/
-public class UserStyleRepository(
+public class CurrentUserStyleRepository(
public val schema: UserStyleSchema
) {
/** A listener for observing [UserStyle] changes. */
- public interface UserStyleListener {
+ public interface UserStyleChangeListener {
/** Called whenever the [UserStyle] changes. */
@UiThread
public fun onUserStyleChanged(userStyle: UserStyle)
}
- private val styleListeners = HashSet<UserStyleListener>()
+ private val styleListeners = HashSet<UserStyleChangeListener>()
- private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id }
+ private val idToStyleSetting = schema.userStyleSettings.associateBy { it.id.value }
/**
- * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
- * callbacks if if any options have changed.
+ * The current [UserStyle]. Assigning to this property triggers immediate
+ * [UserStyleChangeListener] callbacks if if any options have changed.
*/
public var userStyle: UserStyle = UserStyle(
HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
@@ -172,9 +194,9 @@
field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
for ((setting, option) in style.selectedOptions) {
// Ignore an unrecognized setting.
- val localSetting = idToStyleSetting[setting.id] ?: continue
+ val localSetting = idToStyleSetting[setting.id.value] ?: continue
val styleSetting = field.selectedOptions[localSetting] ?: continue
- if (styleSetting.id != option.id) {
+ if (styleSetting.id.value != option.id.value) {
changed = true
}
hashmap[localSetting] = option
@@ -190,19 +212,19 @@
}
/**
- * Adds a [UserStyleListener] which is called immediately and whenever the style changes.
+ * Adds a [UserStyleChangeListener] which is called immediately and whenever the style changes.
*/
@UiThread
@SuppressLint("ExecutorRegistration")
- public fun addUserStyleListener(userStyleListener: UserStyleListener) {
- styleListeners.add(userStyleListener)
- userStyleListener.onUserStyleChanged(userStyle)
+ public fun addUserStyleChangeListener(userStyleChangeListener: UserStyleChangeListener) {
+ styleListeners.add(userStyleChangeListener)
+ userStyleChangeListener.onUserStyleChanged(userStyle)
}
- /** Removes a [UserStyleListener] previously added by [addUserStyleListener]. */
+ /** Removes a [UserStyleChangeListener] previously added by [addUserStyleChangeListener]. */
@UiThread
@SuppressLint("ExecutorRegistration")
- public fun removeUserStyleListener(userStyleListener: UserStyleListener) {
- styleListeners.remove(userStyleListener)
+ public fun removeUserStyleChangeListener(userStyleChangeListener: UserStyleChangeListener) {
+ styleListeners.remove(userStyleChangeListener)
}
}
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
index a23719d..6e378ed 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/Layer.kt
@@ -18,7 +18,12 @@
/** Describes part of watchface. Used as a parameter for rendering. */
public enum class Layer {
- BASE_LAYER,
+ /** The watch excluding complications and anything that may render on top of complications. */
+ BASE,
+
+ /** The watch face complications. */
COMPLICATIONS,
- TOP_LAYER
+
+ /** Anything that may render on top of complications, e.g. watch hands. */
+ COMPLICATIONS_OVERLAY
}
\ No newline at end of file
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index f37b3f8..9fb5fac 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -19,11 +19,8 @@
import android.graphics.drawable.Icon
import androidx.annotation.RestrictTo
import androidx.wear.complications.ComplicationBounds
-import androidx.wear.watchface.style.UserStyleSetting.Companion.maxIdLength
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
-import androidx.wear.watchface.style.UserStyleSetting.Option.Companion.maxIdLength
import androidx.wear.watchface.style.data.BooleanOptionWireFormat
import androidx.wear.watchface.style.data.BooleanUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.ComplicationOverlayWireFormat
@@ -39,6 +36,7 @@
import androidx.wear.watchface.style.data.LongRangeUserStyleSettingWireFormat
import androidx.wear.watchface.style.data.OptionWireFormat
import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
+import java.nio.ByteBuffer
import java.security.InvalidParameterException
/**
@@ -65,23 +63,39 @@
* Companion).
* @param defaultOptionIndex The default option index, used if nothing has been selected within the
* [options] list.
- * @param affectsLayers Used by the style configuration UI. Describes which rendering layers this
+ * @param affectedLayers Used by the style configuration UI. Describes which rendering layers this
* style affects.
*/
public sealed class UserStyleSetting(
- public val id: String,
+ public val id: Id,
public val displayName: CharSequence,
public val description: CharSequence,
public val icon: Icon?,
public val options: List<Option>,
public val defaultOptionIndex: Int,
- public val affectsLayers: Collection<Layer>
+ public val affectedLayers: Collection<Layer>
) {
- public companion object {
- /** Maximum length of the [id] field. */
- @JvmField
- public val maxIdLength: Int = 40
+ /**
+ * Machine readable identifier for [UserStyleSetting]s. The length of this identifier may not
+ * exceed [MAX_LENGTH].
+ */
+ public class Id(public val value: String) {
+ public companion object {
+ /** Maximum length of the [value] field. */
+ public const val MAX_LENGTH: Int = 40
+ }
+ init {
+ require(value.length <= MAX_LENGTH) {
+ "UserStyleSetting.value.length (${value.length}) must be less than MAX_LENGTH " +
+ "($MAX_LENGTH)"
+ }
+ }
+
+ override fun toString(): String = value
+ }
+
+ public companion object {
internal fun createFromWireFormat(
wireFormat: UserStyleSettingWireFormat
): UserStyleSetting = when (wireFormat) {
@@ -108,13 +122,9 @@
require(defaultOptionIndex >= 0 && defaultOptionIndex < options.size) {
"defaultOptionIndex must be in the range [0 .. options.size)"
}
-
- require(id.length <= maxIdLength) {
- "UserStyleSetting id length must not exceed $maxIdLength"
- }
}
- internal fun getSettingOptionForId(id: String?) =
+ internal fun getSettingOptionForId(id: ByteArray?) =
if (id == null) {
options[defaultOptionIndex]
} else {
@@ -122,7 +132,7 @@
}
private constructor(wireFormat: UserStyleSettingWireFormat) : this(
- wireFormat.mId,
+ Id(wireFormat.mId),
wireFormat.mDisplayName,
wireFormat.mDescription,
wireFormat.mIcon,
@@ -143,29 +153,50 @@
/** Returns the default for when the user hasn't selected an option. */
public fun getDefaultOption(): Option = options[defaultOptionIndex]
- override fun toString(): String = "{$id : " + options.joinToString(transform = { it.id }) + "}"
+ override fun toString(): String = "{${id.value} : " +
+ options.joinToString(transform = { it.toString() }) + "}"
/**
* Represents a choice within a style setting which can either be an option from the list or a
* an arbitrary value depending on the nature of the style setting.
*
- * @property id Machine readable identifier for the style setting. Identifier for the option
- * (or the option itself for [CustomValueUserStyleSetting.CustomValueOption]), must be
- * unique within the UserStyleSetting. Short ids are encouraged. There is a maximum
- * length see [maxIdLength].
+ * @property id Machine readable [Id] for the style setting. Identifier for the option (or the
+ * option itself for [CustomValueUserStyleSetting.CustomValueOption]), must be unique
+ * within the UserStyleSetting. Short ids are encouraged.
*/
- public abstract class Option(public val id: String) {
- init {
- require(id.length <= maxIdLength) {
- "UserStyleSetting.Option id length must not exceed $maxIdLength"
+ public abstract class Option(public val id: Id) {
+ /**
+ * Machine readable identifier for [Option]s. The length of this identifier may not exceed
+ * [MAX_LENGTH].
+ */
+ public class Id(public val value: ByteArray) {
+ /**
+ * Constructs an [Id] with a [String] encoded to a [ByteArray] by
+ * [String.encodeToByteArray].
+ */
+ public constructor(value: String) : this(value.encodeToByteArray())
+
+ public companion object {
+ /** Maximum length of the [value] field. */
+ public const val MAX_LENGTH: Int = 1024
}
+
+ init {
+ require(value.size <= MAX_LENGTH) {
+ "Option.Id.value.size (${value.size}) must be less than MAX_LENGTH " +
+ "($MAX_LENGTH)"
+ }
+ }
+
+ override fun toString(): String =
+ try {
+ value.decodeToString()
+ } catch (e: Exception) {
+ value.toString()
+ }
}
public companion object {
- /** Maximum length of the [id] field. */
- @JvmField
- public val maxIdLength: Int = 1024
-
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public fun createFromWireFormat(
@@ -243,6 +274,13 @@
} else {
null
}
+
+ override fun toString(): String =
+ try {
+ id.value.decodeToString()
+ } catch (e: Exception) {
+ id.value.toString()
+ }
}
/**
@@ -255,8 +293,8 @@
* of the UserStyleSetting. If optionName is unrecognized then the default value for the
* setting should be returned.
*/
- public open fun getOptionForId(optionId: String): Option =
- options.find { it.id == optionId } ?: options[defaultOptionIndex]
+ public open fun getOptionForId(optionId: ByteArray): Option =
+ options.find { it.id.value.contentEquals(optionId) } ?: options[defaultOptionIndex]
/** A BooleanUserStyleSetting represents a setting with a true and a false setting. */
public class BooleanUserStyleSetting : UserStyleSetting {
@@ -264,7 +302,7 @@
/**
* Constructs a [BooleanUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the userStyle
* selection UI.
* @param description Localized description string displayed under the displayName.
@@ -274,7 +312,7 @@
* @param defaultValue The default value for this BooleanUserStyleSetting.
*/
public constructor (
- id: String,
+ id: Id,
displayName: CharSequence,
description: CharSequence,
icon: Icon?,
@@ -299,13 +337,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): BooleanUserStyleSettingWireFormat =
BooleanUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
defaultOptionIndex,
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/** Returns the default value. */
@@ -315,20 +353,24 @@
public class BooleanOption : Option {
public val value: Boolean
- public constructor(value: Boolean) : super(value.toString()) {
+ public constructor(value: Boolean) : super(
+ Id(ByteArray(1).apply { this[0] = if (value) 1 else 0 })
+ ) {
this.value = value
}
internal constructor(
wireFormat: BooleanOptionWireFormat
- ) : super(wireFormat.mId) {
- value = wireFormat.mValue
+ ) : super(Id(wireFormat.mId)) {
+ value = wireFormat.mId[0] == 1.toByte()
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): BooleanOptionWireFormat =
- BooleanOptionWireFormat(id, value)
+ BooleanOptionWireFormat(id.value)
+
+ override fun toString(): String = if (id.value[0] == 1.toByte()) "true" else "false"
}
}
@@ -352,7 +394,7 @@
* Overrides to be applied to the corresponding complication's initial config (as specified
* in [androidx.wear.watchface.Complication]) when the setting is selected.
*
- * @param complicationId The id of the complication to configure.
+ * @param complicationId The [Id] of the complication to configure.
* @param enabled If non null, whether the complication should be enabled for this
* configuration. If null then no changes are made.
* @param complicationBounds If non null, the new [ComplicationBounds] for this
@@ -395,12 +437,9 @@
) : this(
wireFormat.mComplicationId,
when (wireFormat.mEnabled) {
- ComplicationOverlayWireFormat
- .ENABLED_UNKNOWN -> null
- ComplicationOverlayWireFormat
- .ENABLED_YES -> true
- ComplicationOverlayWireFormat
- .ENABLED_NO -> false
+ ComplicationOverlayWireFormat.ENABLED_UNKNOWN -> null
+ ComplicationOverlayWireFormat.ENABLED_YES -> true
+ ComplicationOverlayWireFormat.ENABLED_NO -> false
else -> throw InvalidParameterException(
"Unrecognised wireFormat.mEnabled " + wireFormat.mEnabled
)
@@ -419,7 +458,7 @@
/**
* Constructs a [ComplicationsUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the userStyle
* selection UI.
* @param description Localized description string displayed under the displayName.
@@ -432,7 +471,7 @@
*/
@JvmOverloads
public constructor (
- id: String,
+ id: Id,
displayName: CharSequence,
description: CharSequence,
icon: Icon?,
@@ -459,13 +498,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): ComplicationsUserStyleSettingWireFormat =
ComplicationsUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
defaultOptionIndex,
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/** Represents an override to the initial complication configuration. */
@@ -485,7 +524,7 @@
/**
* Constructs a [ComplicationsUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the
* userStyle selection UI.
* @param icon [Icon] for use in the style selection UI.
@@ -494,7 +533,7 @@
* configuration.
*/
public constructor(
- id: String,
+ id: Id,
displayName: CharSequence,
icon: Icon?,
complicationOverlays: Collection<ComplicationOverlay>
@@ -506,7 +545,7 @@
internal constructor(
wireFormat: ComplicationsOptionWireFormat
- ) : super(wireFormat.mId) {
+ ) : super(Id(wireFormat.mId)) {
complicationOverlays =
wireFormat.mComplicationOverlays.map { ComplicationOverlay(it) }
displayName = wireFormat.mDisplayName
@@ -518,7 +557,7 @@
override fun toWireFormat():
ComplicationsOptionWireFormat =
ComplicationsOptionWireFormat(
- id,
+ id.value,
displayName,
icon,
complicationOverlays.map { it.toWireFormat() }.toTypedArray()
@@ -557,7 +596,7 @@
/**
* Constructs a [DoubleRangeUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the
* userStyle selection UI.
* @param description Localized description string displayed under the displayName.
@@ -569,7 +608,7 @@
* @param defaultValue The default value for this DoubleRangeUserStyleSetting.
*/
public constructor (
- id: String,
+ id: Id,
displayName: CharSequence,
description: CharSequence,
icon: Icon?,
@@ -597,13 +636,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): DoubleRangeUserStyleSettingWireFormat =
DoubleRangeUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
defaultOptionIndex,
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/** Represents an option as a [Double] in the range [minimumValue .. maximumValue]. */
@@ -616,23 +655,24 @@
*
* @param value The value of this [DoubleRangeOption]
*/
- public constructor(value: Double) : super(value.toString()) {
+ public constructor(value: Double) : super(
+ Id(ByteArray(8).apply { ByteBuffer.wrap(this).putDouble(value) })
+ ) {
this.value = value
}
internal constructor(
wireFormat: DoubleRangeOptionWireFormat
- ) : super(wireFormat.mId) {
- value = wireFormat.mValue
+ ) : super(Id(wireFormat.mId)) {
+ value = ByteBuffer.wrap(wireFormat.mId).double
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): DoubleRangeOptionWireFormat =
- DoubleRangeOptionWireFormat(
- id,
- value
- )
+ DoubleRangeOptionWireFormat(id.value)
+
+ override fun toString(): String = value.toString()
}
/** Returns the minimum value. */
@@ -646,18 +686,18 @@
(options[defaultOptionIndex] as DoubleRangeOption).value
/** We support all values in the range [min ... max] not just min & max. */
- override fun getOptionForId(optionId: String): Option =
- options.find { it.id == optionId } ?: checkedOptionForId(optionId)
+ override fun getOptionForId(optionId: ByteArray): Option =
+ options.find { it.id.value.contentEquals(optionId) } ?: checkedOptionForId(optionId)
- private fun checkedOptionForId(optionId: String): DoubleRangeOption {
+ private fun checkedOptionForId(optionId: ByteArray): DoubleRangeOption {
return try {
- val value = optionId.toDouble()
+ val value = ByteBuffer.wrap(optionId).double
if (value < getMinimumValue() || value > getMaximumValue()) {
options[defaultOptionIndex] as DoubleRangeOption
} else {
DoubleRangeOption(value)
}
- } catch (e: NumberFormatException) {
+ } catch (e: Exception) {
options[defaultOptionIndex] as DoubleRangeOption
}
}
@@ -669,7 +709,7 @@
/**
* Constructs a [ListUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the userStyle
* selection UI.
* @param description Localized description string displayed under the displayName.
@@ -681,7 +721,7 @@
*/
@JvmOverloads
public constructor (
- id: String,
+ id: Id,
displayName: CharSequence,
description: CharSequence,
icon: Icon?,
@@ -704,13 +744,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): ListUserStyleSettingWireFormat =
ListUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
defaultOptionIndex,
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/**
@@ -726,20 +766,20 @@
/**
* Constructs a [ListOption].
*
- * @param id The id of this [ListOption], must be unique within the
+ * @param id The [Id] of this [ListOption], must be unique within the
* [ListUserStyleSetting].
* @param displayName Localized human readable name for the setting, used in the style
* selection UI.
* @param icon [Icon] for use in the style selection UI.
*/
- public constructor(id: String, displayName: CharSequence, icon: Icon?) : super(id) {
+ public constructor(id: Id, displayName: CharSequence, icon: Icon?) : super(id) {
this.displayName = displayName
this.icon = icon
}
internal constructor(
wireFormat: ListOptionWireFormat
- ) : super(wireFormat.mId) {
+ ) : super(Id(wireFormat.mId)) {
displayName = wireFormat.mDisplayName
icon = wireFormat.mIcon
}
@@ -748,7 +788,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): ListOptionWireFormat =
ListOptionWireFormat(
- id,
+ id.value,
displayName,
icon
)
@@ -789,7 +829,7 @@
/**
* Constructs a [LongRangeUserStyleSetting].
*
- * @param id Identifier for the element, must be unique.
+ * @param id [Id] for the element, must be unique.
* @param displayName Localized human readable name for the element, used in the userStyle
* selection UI.
* @param description Localized description string displayed under the displayName.
@@ -801,7 +841,7 @@
* @param defaultValue The default value for this LongRangeUserStyleSetting.
*/
public constructor (
- id: String,
+ id: Id,
displayName: CharSequence,
description: CharSequence,
icon: Icon?,
@@ -829,13 +869,13 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): LongRangeUserStyleSettingWireFormat =
LongRangeUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
defaultOptionIndex,
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/**
@@ -850,23 +890,24 @@
*
* @param value The value of this [LongRangeOption]
*/
- public constructor(value: Long) : super(value.toString()) {
+ public constructor(value: Long) : super(
+ Id(ByteArray(8).apply { ByteBuffer.wrap(this).putLong(value) })
+ ) {
this.value = value
}
internal constructor(
wireFormat: LongRangeOptionWireFormat
- ) : super(wireFormat.mId) {
- value = wireFormat.mValue
+ ) : super(Id(wireFormat.mId)) {
+ value = ByteBuffer.wrap(wireFormat.mId).long
}
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): LongRangeOptionWireFormat =
- LongRangeOptionWireFormat(
- id,
- value
- )
+ LongRangeOptionWireFormat(id.value)
+
+ override fun toString(): String = value.toString()
}
/**
@@ -887,18 +928,18 @@
/**
* We support all values in the range [min ... max] not just min & max.
*/
- override fun getOptionForId(optionId: String): Option =
- options.find { it.id == optionId } ?: checkedOptionForId(optionId)
+ override fun getOptionForId(optionId: ByteArray): Option =
+ options.find { it.id.value.contentEquals(optionId) } ?: checkedOptionForId(optionId)
- private fun checkedOptionForId(optionId: String): LongRangeOption {
+ private fun checkedOptionForId(optionId: ByteArray): LongRangeOption {
return try {
- val value = optionId.toLong()
+ val value = ByteBuffer.wrap(optionId).long
if (value < getMinimumValue() || value > getMaximumValue()) {
options[defaultOptionIndex] as LongRangeOption
} else {
LongRangeOption(value)
}
- } catch (e: NumberFormatException) {
+ } catch (e: Exception) {
options[defaultOptionIndex] as LongRangeOption
}
}
@@ -918,13 +959,13 @@
*
* @param affectsLayers Used by the style configuration UI. Describes which rendering layers
* this style affects.
- * @param defaultValue The default value.
+ * @param defaultValue The default value [ByteArray].
*/
public constructor (
affectsLayers: Collection<Layer>,
- defaultValue: String
+ defaultValue: ByteArray
) : super(
- CUSTOM_VALUE_USER_STYLE_SETTING_ID,
+ Id(CUSTOM_VALUE_USER_STYLE_SETTING_ID),
"",
"",
null,
@@ -939,12 +980,12 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): CustomValueUserStyleSettingWireFormat =
CustomValueUserStyleSettingWireFormat(
- id,
+ id.value,
displayName,
description,
icon,
getWireFormatOptionsList(),
- affectsLayers.map { it.ordinal }
+ affectedLayers.map { it.ordinal }
)
/**
@@ -952,28 +993,29 @@
* same as the [CustomValueOption.id].
*/
public class CustomValueOption : Option {
- /* The value for this option which is the same as the [id]. */
- public val customValue: String
- get() = id
+ /* The [ByteArray] value for this option which is the same as the [id]. */
+ public val customValue: ByteArray
+ get() = id.value
/**
* Constructs a [CustomValueOption].
*
- * @param customValue The [id] and value of this [CustomValueOption].
+ * @param customValue The [ByteArray] [id] and value of this [CustomValueOption]. This
+ * may not exceed [Id.MAX_LENGTH].
*/
- public constructor(customValue: String) : super(customValue)
+ public constructor(customValue: ByteArray) : super(Id(customValue))
internal constructor(
wireFormat: CustomValueOptionWireFormat
- ) : super(wireFormat.mId)
+ ) : super(Id(wireFormat.mId))
/** @hide */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
override fun toWireFormat(): CustomValueOptionWireFormat =
- CustomValueOptionWireFormat(id)
+ CustomValueOptionWireFormat(id.value)
}
- override fun getOptionForId(optionId: String): Option =
- options.find { it.id == optionId } ?: CustomValueOption(optionId)
+ override fun getOptionForId(optionId: ByteArray): Option =
+ options.find { it.id.value.contentEquals(optionId) } ?: CustomValueOption(optionId)
}
}
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
similarity index 61%
rename from wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
rename to wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 3efa3ea..8d88841 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleRepositoryTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -19,6 +19,7 @@
import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
@@ -26,81 +27,84 @@
import org.mockito.Mockito
@RunWith(StyleTestRunner::class)
-class UserStyleRepositoryTest {
+public class CurrentUserStyleRepositoryTest {
private val redStyleOption =
- ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
private val greenStyleOption =
- ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
private val blueStyleOption =
- ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
private val colorStyleSetting = ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
private val classicStyleOption =
- ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
private val modernStyleOption =
- ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
private val gothicStyleOption =
- ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
private val watchHandStyleSetting = ListUserStyleSetting(
- "hand_style_setting",
+ UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
private val watchHandLengthStyleSetting =
DoubleRangeUserStyleSetting(
- "watch_hand_length_style_setting",
+ UserStyleSetting.Id("watch_hand_length_style_setting"),
"Hand length",
"Scale of watch hands",
null,
0.25,
1.0,
- listOf(Layer.TOP_LAYER),
+ listOf(Layer.COMPLICATIONS_OVERLAY),
0.75
)
- private val mockListener1 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
- private val mockListener2 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
- private val mockListener3 = Mockito.mock(UserStyleRepository.UserStyleListener::class.java)
+ private val mockListener1 =
+ Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
+ private val mockListener2 =
+ Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
+ private val mockListener3 =
+ Mockito.mock(CurrentUserStyleRepository.UserStyleChangeListener::class.java)
private val userStyleRepository =
- UserStyleRepository(
+ CurrentUserStyleRepository(
UserStyleSchema(
listOf(colorStyleSetting, watchHandStyleSetting, watchHandLengthStyleSetting)
)
)
@Test
- fun addUserStyleListener_firesImmediately() {
- userStyleRepository.addUserStyleListener(mockListener1)
+ public fun addUserStyleListener_firesImmediately() {
+ userStyleRepository.addUserStyleChangeListener(mockListener1)
Mockito.verify(mockListener1).onUserStyleChanged(userStyleRepository.userStyle)
}
@Test
- fun assigning_userStyle_firesListeners() {
- userStyleRepository.addUserStyleListener(mockListener1)
- userStyleRepository.addUserStyleListener(mockListener2)
- userStyleRepository.addUserStyleListener(mockListener3)
+ public fun assigning_userStyle_firesListeners() {
+ userStyleRepository.addUserStyleChangeListener(mockListener1)
+ userStyleRepository.addUserStyleChangeListener(mockListener2)
+ userStyleRepository.addUserStyleChangeListener(mockListener3)
Mockito.verify(mockListener1).onUserStyleChanged(userStyleRepository.userStyle)
Mockito.verify(mockListener2).onUserStyleChanged(userStyleRepository.userStyle)
@@ -124,7 +128,7 @@
Mockito.verify(mockListener3).onUserStyleChanged(userStyleRepository.userStyle)
}
- fun assigning_userStyle() {
+ public fun assigning_userStyle() {
val newStyle = UserStyle(
hashMapOf(
colorStyleSetting to greenStyleOption,
@@ -141,22 +145,22 @@
}
@Test
- fun assign_userStyle_with_distinctButMatchingRefs() {
+ public fun assign_userStyle_with_distinctButMatchingRefs() {
val colorStyleSetting2 = ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val watchHandStyleSetting2 = ListUserStyleSetting(
- "hand_style_setting",
+ UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
val newStyle = UserStyle(
@@ -175,7 +179,7 @@
}
@Test
- fun defaultValues() {
+ public fun defaultValues() {
val watchHandLengthOption =
userStyleRepository.userStyle.selectedOptions[watchHandLengthStyleSetting]!! as
DoubleRangeUserStyleSetting.DoubleRangeOption
@@ -183,80 +187,93 @@
}
@Test
- fun userStyle_mapConstructor() {
+ public fun userStyle_mapConstructor() {
val userStyle = UserStyle(
- mapOf(
- "color_style_setting" to "bluestyle",
- "hand_style_setting" to "gothic_style"
+ UserStyleData(
+ mapOf(
+ "color_style_setting" to "bluestyle".encodeToByteArray(),
+ "hand_style_setting" to "gothic_style".encodeToByteArray()
+ )
),
userStyleRepository.schema
)
- assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("bluestyle")
- assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+ assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("bluestyle")
+ assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("gothic_style")
}
@Test
- fun userStyle_mapConstructor_badColorStyle() {
+ public fun userStyle_mapConstructor_badColorStyle() {
val userStyle = UserStyle(
- mapOf(
- "color_style_setting" to "I DO NOT EXIST",
- "hand_style_setting" to "gothic_style"
+ UserStyleData(
+ mapOf(
+ "color_style_setting" to "I DO NOT EXIST".encodeToByteArray(),
+ "hand_style_setting" to "gothic_style".encodeToByteArray()
+ )
),
userStyleRepository.schema
)
- assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("red_style")
- assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+ assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("red_style")
+ assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("gothic_style")
}
@Test
- fun userStyle_mapConstructor_missingColorStyle() {
+ public fun userStyle_mapConstructor_missingColorStyle() {
val userStyle = UserStyle(
- mapOf(
- "hand_style_setting" to "gothic_style"
+ UserStyleData(
+ mapOf("hand_style_setting" to "gothic_style".encodeToByteArray())
),
userStyleRepository.schema
)
- assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id).isEqualTo("red_style")
- assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id).isEqualTo("gothic_style")
+ assertThat(userStyle.selectedOptions[colorStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("red_style")
+ assertThat(userStyle.selectedOptions[watchHandStyleSetting]!!.id.value.decodeToString())
+ .isEqualTo("gothic_style")
}
@Test
- fun userStyle_mapConstructor_customValueUserStyleSetting() {
+ public fun userStyle_mapConstructor_customValueUserStyleSetting() {
val customStyleSetting = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(
listOf(customStyleSetting)
)
)
val userStyle = UserStyle(
- mapOf(
- CustomValueUserStyleSetting.CUSTOM_VALUE_USER_STYLE_SETTING_ID to "TEST 123"
+ UserStyleData(
+ mapOf(
+ CustomValueUserStyleSetting.CUSTOM_VALUE_USER_STYLE_SETTING_ID
+ to "TEST 123".encodeToByteArray()
+ )
),
userStyleRepository.schema
)
val customValue = userStyle.selectedOptions[customStyleSetting]!! as
UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
- assertThat(customValue.customValue).isEqualTo("TEST 123")
+ assertThat(customValue.customValue.decodeToString()).isEqualTo("TEST 123")
}
@Test
- fun userStyle_multiple_CustomValueUserStyleSettings_notAllowed() {
+ public fun userStyle_multiple_CustomValueUserStyleSettings_notAllowed() {
val customStyleSetting1 = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
val customStyleSetting2 = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
try {
@@ -273,24 +290,29 @@
}
@Test
- fun setAndGetCustomStyleSetting() {
+ public fun setAndGetCustomStyleSetting() {
val customStyleSetting = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(
listOf(customStyleSetting)
)
)
userStyleRepository.userStyle = UserStyle(
- mapOf(customStyleSetting to CustomValueUserStyleSetting.CustomValueOption("test"))
+ mapOf(
+ customStyleSetting to
+ CustomValueUserStyleSetting.CustomValueOption("test".encodeToByteArray())
+ )
)
assertThat(
- userStyleRepository.userStyle[customStyleSetting]?.toCustomValueOption()!!.customValue
+ userStyleRepository.userStyle[
+ customStyleSetting
+ ]?.toCustomValueOption()!!.customValue.decodeToString()
).isEqualTo("test")
}
}
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
index 57b4cdc..322f0f0 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleParcelableTest.kt
@@ -25,6 +25,7 @@
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
import androidx.wear.watchface.style.data.UserStyleSettingWireFormat
import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -36,27 +37,27 @@
import org.junit.runner.RunWith
@RunWith(StyleTestRunner::class)
-class StyleParcelableTest {
+public class StyleParcelableTest {
private val icon1 = Icon.createWithContentUri("icon1")
private val icon2 = Icon.createWithContentUri("icon2")
private val icon3 = Icon.createWithContentUri("icon3")
private val icon4 = Icon.createWithContentUri("icon4")
- private val option1 = ListUserStyleSetting.ListOption("1", "one", icon1)
- private val option2 = ListUserStyleSetting.ListOption("2", "two", icon2)
- private val option3 = ListUserStyleSetting.ListOption("3", "three", icon3)
- private val option4 = ListUserStyleSetting.ListOption("4", "four", icon4)
+ private val option1 = ListUserStyleSetting.ListOption(Option.Id("1"), "one", icon1)
+ private val option2 = ListUserStyleSetting.ListOption(Option.Id("2"), "two", icon2)
+ private val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "three", icon3)
+ private val option4 = ListUserStyleSetting.ListOption(Option.Id("4"), "four", icon4)
@Test
- fun parcelAndUnparcelStyleSettingAndOption() {
+ public fun parcelAndUnparcelStyleSettingAndOption() {
val settingIcon = Icon.createWithContentUri("settingIcon")
val styleSetting = ListUserStyleSetting(
- "id",
+ UserStyleSetting.Id("id"),
"displayName",
"description",
settingIcon,
listOf(option1, option2, option3),
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val parcel = Parcel.obtain()
@@ -72,29 +73,29 @@
assert(unparceled is ListUserStyleSetting)
- assertThat(unparceled.id).isEqualTo("id")
+ assertThat(unparceled.id.value).isEqualTo("id")
assertThat(unparceled.displayName).isEqualTo("displayName")
assertThat(unparceled.description).isEqualTo("description")
assertThat(unparceled.icon!!.uri.toString()).isEqualTo("settingIcon")
- assertThat(unparceled.affectsLayers.size).isEqualTo(1)
- assertThat(unparceled.affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+ assertThat(unparceled.affectedLayers.size).isEqualTo(1)
+ assertThat(unparceled.affectedLayers.first()).isEqualTo(Layer.BASE)
val optionArray =
unparceled.options.filterIsInstance<ListUserStyleSetting.ListOption>()
.toTypedArray()
assertThat(optionArray.size).isEqualTo(3)
- assertThat(optionArray[0].id).isEqualTo("1")
+ assertThat(optionArray[0].id.value.decodeToString()).isEqualTo("1")
assertThat(optionArray[0].displayName).isEqualTo("one")
assertThat(optionArray[0].icon!!.uri.toString()).isEqualTo("icon1")
- assertThat(optionArray[1].id).isEqualTo("2")
+ assertThat(optionArray[1].id.value.decodeToString()).isEqualTo("2")
assertThat(optionArray[1].displayName).isEqualTo("two")
assertThat(optionArray[1].icon!!.uri.toString()).isEqualTo("icon2")
- assertThat(optionArray[2].id).isEqualTo("3")
+ assertThat(optionArray[2].id.value.decodeToString()).isEqualTo("3")
assertThat(optionArray[2].displayName).isEqualTo("three")
assertThat(optionArray[2].icon!!.uri.toString()).isEqualTo("icon3")
}
@Test
- fun marshallAndUnmarshallOptions() {
+ public fun marshallAndUnmarshallOptions() {
val wireFormat1 = option1.toWireFormat()
val wireFormat2 = option2.toWireFormat()
val wireFormat3 = option3.toWireFormat()
@@ -106,48 +107,48 @@
val unmarshalled3 =
UserStyleSetting.Option.createFromWireFormat(wireFormat3).toListOption()!!
- assertThat(unmarshalled1.id).isEqualTo("1")
+ assertThat(unmarshalled1.id.value.decodeToString()).isEqualTo("1")
assertThat(unmarshalled1.displayName).isEqualTo("one")
assertThat(unmarshalled1.icon!!.uri.toString()).isEqualTo("icon1")
- assertThat(unmarshalled2.id).isEqualTo("2")
+ assertThat(unmarshalled2.id.value.decodeToString()).isEqualTo("2")
assertThat(unmarshalled2.displayName).isEqualTo("two")
assertThat(unmarshalled2.icon!!.uri.toString()).isEqualTo("icon2")
- assertThat(unmarshalled3.id).isEqualTo("3")
+ assertThat(unmarshalled3.id.value.decodeToString()).isEqualTo("3")
assertThat(unmarshalled3.displayName).isEqualTo("three")
assertThat(unmarshalled3.icon!!.uri.toString()).isEqualTo("icon3")
}
@Test
- fun parcelAndUnparcelUserStyleSchema() {
+ public fun parcelAndUnparcelUserStyleSchema() {
val settingIcon1 = Icon.createWithContentUri("settingIcon1")
val settingIcon2 = Icon.createWithContentUri("settingIcon2")
val styleSetting1 = ListUserStyleSetting(
- "id1",
+ UserStyleSetting.Id("id1"),
"displayName1",
"description1",
settingIcon1,
listOf(option1, option2),
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val styleSetting2 = ListUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
settingIcon2,
listOf(option3, option4),
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
val styleSetting3 = BooleanUserStyleSetting(
- "id3",
+ UserStyleSetting.Id("id3"),
"displayName3",
"description3",
null,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
true
)
val styleSetting4 = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
val srcSchema = UserStyleSchema(
@@ -169,74 +170,77 @@
parcel.recycle()
assert(schema.userStyleSettings[0] is ListUserStyleSetting)
- assertThat(schema.userStyleSettings[0].id).isEqualTo("id1")
+ assertThat(schema.userStyleSettings[0].id.value).isEqualTo("id1")
assertThat(schema.userStyleSettings[0].displayName).isEqualTo("displayName1")
assertThat(schema.userStyleSettings[0].description).isEqualTo("description1")
assertThat(schema.userStyleSettings[0].icon!!.uri.toString()).isEqualTo("settingIcon1")
- assertThat(schema.userStyleSettings[0].affectsLayers.size).isEqualTo(1)
- assertThat(schema.userStyleSettings[0].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+ assertThat(schema.userStyleSettings[0].affectedLayers.size).isEqualTo(1)
+ assertThat(schema.userStyleSettings[0].affectedLayers.first()).isEqualTo(Layer.BASE)
val optionArray1 =
schema.userStyleSettings[0].options.filterIsInstance<ListUserStyleSetting.ListOption>()
.toTypedArray()
assertThat(optionArray1.size).isEqualTo(2)
- assertThat(optionArray1[0].id).isEqualTo("1")
+ assertThat(optionArray1[0].id.value.decodeToString()).isEqualTo("1")
assertThat(optionArray1[0].displayName).isEqualTo("one")
assertThat(optionArray1[0].icon!!.uri.toString()).isEqualTo("icon1")
- assertThat(optionArray1[1].id).isEqualTo("2")
+ assertThat(optionArray1[1].id.value.decodeToString()).isEqualTo("2")
assertThat(optionArray1[1].displayName).isEqualTo("two")
assertThat(optionArray1[1].icon!!.uri.toString()).isEqualTo("icon2")
assert(schema.userStyleSettings[1] is ListUserStyleSetting)
- assertThat(schema.userStyleSettings[1].id).isEqualTo("id2")
+ assertThat(schema.userStyleSettings[1].id.value).isEqualTo("id2")
assertThat(schema.userStyleSettings[1].displayName).isEqualTo("displayName2")
assertThat(schema.userStyleSettings[1].description).isEqualTo("description2")
assertThat(schema.userStyleSettings[1].icon!!.uri.toString()).isEqualTo("settingIcon2")
- assertThat(schema.userStyleSettings[1].affectsLayers.size).isEqualTo(1)
- assertThat(schema.userStyleSettings[1].affectsLayers.first()).isEqualTo(Layer.TOP_LAYER)
+ assertThat(schema.userStyleSettings[1].affectedLayers.size).isEqualTo(1)
+ assertThat(schema.userStyleSettings[1].affectedLayers.first()).isEqualTo(
+ Layer.COMPLICATIONS_OVERLAY
+ )
val optionArray2 =
schema.userStyleSettings[1].options.filterIsInstance<ListUserStyleSetting.ListOption>()
.toTypedArray()
assertThat(optionArray2.size).isEqualTo(2)
- assertThat(optionArray2[0].id).isEqualTo("3")
+ assertThat(optionArray2[0].id.value.decodeToString()).isEqualTo("3")
assertThat(optionArray2[0].displayName).isEqualTo("three")
assertThat(optionArray2[0].icon!!.uri.toString()).isEqualTo("icon3")
- assertThat(optionArray2[1].id).isEqualTo("4")
+ assertThat(optionArray2[1].id.value.decodeToString()).isEqualTo("4")
assertThat(optionArray2[1].displayName).isEqualTo("four")
assertThat(optionArray2[1].icon!!.uri.toString()).isEqualTo("icon4")
assert(schema.userStyleSettings[2] is BooleanUserStyleSetting)
- assertThat(schema.userStyleSettings[2].id).isEqualTo("id3")
+ assertThat(schema.userStyleSettings[2].id.value).isEqualTo("id3")
assertThat(schema.userStyleSettings[2].displayName).isEqualTo("displayName3")
assertThat(schema.userStyleSettings[2].description).isEqualTo("description3")
assertThat(schema.userStyleSettings[2].icon).isEqualTo(null)
- assertThat(schema.userStyleSettings[2].affectsLayers.size).isEqualTo(1)
- assertThat(schema.userStyleSettings[2].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+ assertThat(schema.userStyleSettings[2].affectedLayers.size).isEqualTo(1)
+ assertThat(schema.userStyleSettings[2].affectedLayers.first()).isEqualTo(Layer.BASE)
assert(schema.userStyleSettings[3] is CustomValueUserStyleSetting)
- assertThat(schema.userStyleSettings[3].getDefaultOption().id).isEqualTo("default")
- assertThat(schema.userStyleSettings[3].affectsLayers.size).isEqualTo(1)
- assertThat(schema.userStyleSettings[3].affectsLayers.first()).isEqualTo(Layer.BASE_LAYER)
+ assertThat(schema.userStyleSettings[3].getDefaultOption().id.value.decodeToString())
+ .isEqualTo("default")
+ assertThat(schema.userStyleSettings[3].affectedLayers.size).isEqualTo(1)
+ assertThat(schema.userStyleSettings[3].affectedLayers.first()).isEqualTo(Layer.BASE)
}
@Test
- fun parcelAndUnparcelUserStyle() {
+ public fun parcelAndUnparcelUserStyle() {
val settingIcon1 = Icon.createWithContentUri("settingIcon1")
val settingIcon2 = Icon.createWithContentUri("settingIcon2")
val styleSetting1 = ListUserStyleSetting(
- "id1",
+ UserStyleSetting.Id("id1"),
"displayName1",
"description1",
settingIcon1,
listOf(option1, option2),
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val styleSetting2 = ListUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
settingIcon2,
listOf(option3, option4),
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
val schema = UserStyleSchema(listOf(styleSetting1, styleSetting2))
val userStyle = UserStyle(
@@ -252,133 +256,135 @@
parcel.setDataPosition(0)
val unparcelled =
- UserStyle(UserStyleWireFormat.CREATOR.createFromParcel(parcel), schema)
+ UserStyle(UserStyleData(UserStyleWireFormat.CREATOR.createFromParcel(parcel)), schema)
parcel.recycle()
assertThat(unparcelled.selectedOptions.size).isEqualTo(2)
- assertThat(unparcelled.selectedOptions[styleSetting1]!!.id).isEqualTo(option2.id)
- assertThat(unparcelled.selectedOptions[styleSetting2]!!.id).isEqualTo(option3.id)
+ assertThat(unparcelled.selectedOptions[styleSetting1]!!.id.value.decodeToString())
+ .isEqualTo(option2.id.value.decodeToString())
+ assertThat(unparcelled.selectedOptions[styleSetting2]!!.id.value.decodeToString())
+ .isEqualTo(option3.id.value.decodeToString())
}
@Test
- fun booleanUserStyleSetting_defaultValue() {
+ public fun booleanUserStyleSetting_defaultValue() {
val booleanUserStyleSettingDefaultTrue = BooleanUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
true
)
assertTrue(booleanUserStyleSettingDefaultTrue.getDefaultValue())
val booleanUserStyleSettingDefaultFalse = BooleanUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
false
)
assertFalse(booleanUserStyleSettingDefaultFalse.getDefaultValue())
}
@Test
- fun doubleRangeUserStyleSetting_defaultValue() {
+ public fun doubleRangeUserStyleSetting_defaultValue() {
val doubleRangeUserStyleSettingDefaultMin = DoubleRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1.0,
1.0,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
-1.0
)
assertThat(doubleRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1.0)
val doubleRangeUserStyleSettingDefaultMid = DoubleRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1.0,
1.0,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
0.5
)
assertThat(doubleRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(0.5)
val doubleRangeUserStyleSettingDefaultMax = DoubleRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1.0,
1.0,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
1.0
)
assertThat(doubleRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(1.0)
}
@Test
- fun longRangeUserStyleSetting_defaultValue() {
+ public fun longRangeUserStyleSetting_defaultValue() {
val longRangeUserStyleSettingDefaultMin = LongRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1,
10,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
-1,
)
assertThat(longRangeUserStyleSettingDefaultMin.getDefaultValue()).isEqualTo(-1)
val longRangeUserStyleSettingDefaultMid = LongRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1,
10,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
5
)
assertThat(longRangeUserStyleSettingDefaultMid.getDefaultValue()).isEqualTo(5)
val longRangeUserStyleSettingDefaultMax = LongRangeUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
null,
-1,
10,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
10
)
assertThat(longRangeUserStyleSettingDefaultMax.getDefaultValue()).isEqualTo(10)
}
@Test
- fun parcelAndUnparcelComplicationsUserStyleSetting() {
+ public fun parcelAndUnparcelComplicationsUserStyleSetting() {
val leftComplicationID = 101
val rightComplicationID = 102
val src = ComplicationsUserStyleSetting(
- "complications_style_setting",
+ UserStyleSetting.Id("complications_style_setting"),
"Complications",
"Number and position",
icon = null,
complicationConfig = listOf(
ComplicationsUserStyleSetting.ComplicationsOption(
- "LEFT_AND_RIGHT_COMPLICATIONS",
+ Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
"Both",
null,
listOf()
),
ComplicationsUserStyleSetting.ComplicationsOption(
- "NO_COMPLICATIONS",
+ Option.Id("NO_COMPLICATIONS"),
"None",
null,
listOf(
@@ -393,7 +399,7 @@
)
),
ComplicationsUserStyleSetting.ComplicationsOption(
- "LEFT_COMPLICATION",
+ Option.Id("LEFT_COMPLICATION"),
"Left",
null,
listOf(
@@ -404,7 +410,7 @@
)
),
ComplicationsUserStyleSetting.ComplicationsOption(
- "RIGHT_COMPLICATION",
+ Option.Id("RIGHT_COMPLICATION"),
"Right",
null,
listOf(
@@ -430,15 +436,15 @@
parcel.recycle()
assert(unparceled is ComplicationsUserStyleSetting)
- assertThat(unparceled.id).isEqualTo("complications_style_setting")
+ assertThat(unparceled.id.value).isEqualTo("complications_style_setting")
val options = unparceled.options.filterIsInstance<
ComplicationsUserStyleSetting.ComplicationsOption>()
assertThat(options.size).isEqualTo(4)
- assertThat(options[0].id).isEqualTo("LEFT_AND_RIGHT_COMPLICATIONS")
+ assertThat(options[0].id.value.decodeToString()).isEqualTo("LEFT_AND_RIGHT_COMPLICATIONS")
assertThat(options[0].complicationOverlays.size).isEqualTo(0)
- assertThat(options[1].id).isEqualTo("NO_COMPLICATIONS")
+ assertThat(options[1].id.value.decodeToString()).isEqualTo("NO_COMPLICATIONS")
assertThat(options[1].complicationOverlays.size).isEqualTo(2)
val options1Overlays = ArrayList(options[1].complicationOverlays)
assertThat(options1Overlays[0].complicationId).isEqualTo(leftComplicationID)
@@ -446,13 +452,13 @@
assertThat(options1Overlays[1].complicationId).isEqualTo(rightComplicationID)
assertFalse(options1Overlays[1].enabled!!)
- assertThat(options[2].id).isEqualTo("LEFT_COMPLICATION")
+ assertThat(options[2].id.value.decodeToString()).isEqualTo("LEFT_COMPLICATION")
assertThat(options[2].complicationOverlays.size).isEqualTo(1)
val options2Overlays = ArrayList(options[2].complicationOverlays)
assertThat(options2Overlays[0].complicationId).isEqualTo(rightComplicationID)
assertFalse(options2Overlays[0].enabled!!)
- assertThat(options[3].id).isEqualTo("RIGHT_COMPLICATION")
+ assertThat(options[3].id.value.decodeToString()).isEqualTo("RIGHT_COMPLICATION")
assertThat(options[3].complicationOverlays.size).isEqualTo(1)
val options3Overlays = ArrayList(options[3].complicationOverlays)
assertThat(options3Overlays[0].complicationId).isEqualTo(leftComplicationID)
@@ -460,36 +466,36 @@
}
@Test
- fun styleSchemaToString() {
+ public fun styleSchemaToString() {
val settingIcon1 = Icon.createWithContentUri("settingIcon1")
val settingIcon2 = Icon.createWithContentUri("settingIcon2")
val styleSetting1 = ListUserStyleSetting(
- "id1",
+ UserStyleSetting.Id("id1"),
"displayName1",
"description1",
settingIcon1,
listOf(option1, option2),
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val styleSetting2 = ListUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
settingIcon2,
listOf(option3, option4),
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
val styleSetting3 = BooleanUserStyleSetting(
- "id3",
+ UserStyleSetting.Id("id3"),
"displayName3",
"description3",
null,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
true
)
val styleSetting4 = CustomValueUserStyleSetting(
- listOf(Layer.BASE_LAYER),
- "default"
+ listOf(Layer.BASE),
+ "default".encodeToByteArray()
)
val schema = UserStyleSchema(
@@ -508,24 +514,24 @@
@Ignore
@Test
- fun userStyleToString() {
+ public fun userStyleToString() {
val settingIcon1 = Icon.createWithContentUri("settingIcon1")
val settingIcon2 = Icon.createWithContentUri("settingIcon2")
val styleSetting1 = ListUserStyleSetting(
- "id1",
+ UserStyleSetting.Id("id1"),
"displayName1",
"description1",
settingIcon1,
listOf(option1, option2),
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
val styleSetting2 = ListUserStyleSetting(
- "id2",
+ UserStyleSetting.Id("id2"),
"displayName2",
"description2",
settingIcon2,
listOf(option3, option4),
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
val style = UserStyle(
mapOf(
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
index fef3704..3d018cd 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/StyleTestRunner.kt
@@ -22,7 +22,7 @@
// Without this we get test failures with an error:
// "failed to access class kotlin.jvm.internal.DefaultConstructorMarker".
-class StyleTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
+public class StyleTestRunner(testClass: Class<*>) : RobolectricTestRunner(testClass) {
override fun createClassLoaderConfig(method: FrameworkMethod): InstrumentationConfiguration =
InstrumentationConfiguration.Builder(
super.createClassLoaderConfig(method)
diff --git a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
index e5965cb..742850d 100644
--- a/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
+++ b/wear/wear-watchface-style/src/test/java/androidx/wear/watchface/style/UserStyleSettingTest.kt
@@ -16,91 +16,105 @@
package androidx.wear.watchface.style
+import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.fail
import org.junit.Test
import org.junit.runner.RunWith
+import java.nio.ByteBuffer
@RunWith(StyleTestRunner::class)
-class UserStyleSettingTest {
+public class UserStyleSettingTest {
+
+ private fun doubleToByteArray(value: Double) =
+ ByteArray(8).apply { ByteBuffer.wrap(this).putDouble(value) }
+
+ private fun byteArrayToDouble(value: ByteArray) = ByteBuffer.wrap(value).double
@Test
- fun rangedUserStyleSetting_getOptionForId_returns_default_for_bad_input() {
+ public fun rangedUserStyleSetting_getOptionForId_returns_default_for_bad_input() {
val defaultValue = 0.75
val rangedUserStyleSetting =
DoubleRangeUserStyleSetting(
- "example_setting",
+ UserStyleSetting.Id("example_setting"),
"Example Ranged Setting",
"An example setting",
null,
0.0,
1.0,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
defaultValue
)
- assertThat(rangedUserStyleSetting.getOptionForId("not a number").id)
- .isEqualTo(defaultValue.toString())
+ assertThat(
+ rangedUserStyleSetting.getOptionForId("not a number".encodeToByteArray())
+ .toDoubleRangeOption()!!.value
+ ).isEqualTo(defaultValue)
- assertThat(rangedUserStyleSetting.getOptionForId("-1").id)
- .isEqualTo(defaultValue.toString())
+ assertThat(
+ rangedUserStyleSetting.getOptionForId("-1".encodeToByteArray())
+ .toDoubleRangeOption()!!.value
+ ).isEqualTo(defaultValue)
- assertThat(rangedUserStyleSetting.getOptionForId("10").id)
- .isEqualTo(defaultValue.toString())
+ assertThat(
+ rangedUserStyleSetting.getOptionForId("10".encodeToByteArray())
+ .toDoubleRangeOption()!!.value
+ ).isEqualTo(defaultValue)
}
@Test
- fun rangedUserStyleSetting_getOptionForId() {
+ public fun byteArrayConversion() {
+ assertThat(BooleanUserStyleSetting.BooleanOption(true).value).isEqualTo(true)
+ assertThat(BooleanUserStyleSetting.BooleanOption(false).value).isEqualTo(false)
+ assertThat(DoubleRangeUserStyleSetting.DoubleRangeOption(123.4).value).isEqualTo(123.4)
+ assertThat(LongRangeUserStyleSetting.LongRangeOption(1234).value).isEqualTo(1234)
+ }
+
+ @Test
+ public fun rangedUserStyleSetting_getOptionForId() {
val defaultValue = 0.75
val rangedUserStyleSetting =
DoubleRangeUserStyleSetting(
- "example_setting",
+ UserStyleSetting.Id("example_setting"),
"Example Ranged Setting",
"An example setting",
null,
0.0,
1.0,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
defaultValue
)
- assertThat(rangedUserStyleSetting.getOptionForId("0").id)
- .isEqualTo("0.0")
+ assertThat(
+ byteArrayToDouble(
+ rangedUserStyleSetting.getOptionForId(doubleToByteArray(0.0)).id.value
+ )
+ ).isEqualTo(0.0)
- assertThat(rangedUserStyleSetting.getOptionForId("0.5").id)
- .isEqualTo("0.5")
+ assertThat(
+ byteArrayToDouble(
+ rangedUserStyleSetting.getOptionForId(doubleToByteArray(0.5)).id.value
+ )
+ ).isEqualTo(0.5)
- assertThat(rangedUserStyleSetting.getOptionForId("1").id)
- .isEqualTo("1.0")
+ assertThat(
+ byteArrayToDouble(
+ rangedUserStyleSetting.getOptionForId(doubleToByteArray(1.0)).id.value
+ )
+ ).isEqualTo(1.0)
}
@Test
- fun maximumUserStyleSettingIdLength() {
+ public fun maximumUserStyleSettingIdLength() {
// OK.
- DoubleRangeUserStyleSetting(
- "x".repeat(UserStyleSetting.maxIdLength),
- "",
- "",
- null,
- 0.0,
- 1.0,
- emptyList(),
- 1.0
- )
+ UserStyleSetting.Id("x".repeat(UserStyleSetting.Id.MAX_LENGTH))
try {
// Not OK.
- DoubleRangeUserStyleSetting(
- "x".repeat(UserStyleSetting.maxIdLength + 1),
- "",
- "",
- null,
- 0.0,
- 1.0,
- emptyList(),
- 1.0
- )
+ UserStyleSetting.Id("x".repeat(UserStyleSetting.Id.MAX_LENGTH + 1))
fail("Should have thrown an exception")
} catch (e: Exception) {
// Expected
@@ -108,21 +122,13 @@
}
@Test
- fun maximumOptionIdLength() {
+ public fun maximumOptionIdLength() {
// OK.
- UserStyleSetting.ListUserStyleSetting.ListOption(
- "x".repeat(UserStyleSetting.Option.maxIdLength),
- "",
- null
- )
+ Option.Id("x".repeat(Option.Id.MAX_LENGTH))
try {
// Not OK.
- UserStyleSetting.ListUserStyleSetting.ListOption(
- "x".repeat(UserStyleSetting.Option.maxIdLength + 1),
- "",
- null
- )
+ Option.Id("x".repeat(Option.Id.MAX_LENGTH + 1))
fail("Should have thrown an exception")
} catch (e: Exception) {
// Expected
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index 009f4e8..a489434 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -72,7 +72,7 @@
}
public final class ComplicationsManager {
- ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+ ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
method @UiThread public void displayPressedAnimation(int complicationId);
method public operator androidx.wear.watchface.Complication? get(int id);
@@ -176,14 +176,14 @@
}
public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
}
public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method public final android.opengl.EGLConfig getEglConfig();
method public final android.opengl.EGLContext? getEglContext();
method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -203,18 +203,18 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+ method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method public Long? getOverridePreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
method public static boolean isLegacyWatchFaceOverlayStyleSupported();
method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+ property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
property public final Long? overridePreviewReferenceTimeMillis;
- property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
field public static final androidx.wear.watchface.WatchFace.Companion Companion;
}
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index 009f4e8..a489434 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -72,7 +72,7 @@
}
public final class ComplicationsManager {
- ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+ ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
method @UiThread public void displayPressedAnimation(int complicationId);
method public operator androidx.wear.watchface.Complication? get(int id);
@@ -176,14 +176,14 @@
}
public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
}
public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method public final android.opengl.EGLConfig getEglConfig();
method public final android.opengl.EGLContext? getEglContext();
method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -203,18 +203,18 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+ method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method public Long? getOverridePreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
method public static boolean isLegacyWatchFaceOverlayStyleSupported();
method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+ property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
property public final Long? overridePreviewReferenceTimeMillis;
- property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
field public static final androidx.wear.watchface.WatchFace.Companion Companion;
}
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index 151e250..33f3560 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -72,7 +72,7 @@
}
public final class ComplicationsManager {
- ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.UserStyleRepository userStyleRepository);
+ ctor public ComplicationsManager(java.util.Collection<androidx.wear.watchface.Complication> complicationCollection, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository);
method @UiThread public void addTapListener(androidx.wear.watchface.ComplicationsManager.TapCallback tapCallback);
method @UiThread public void displayPressedAnimation(int complicationId);
method public operator androidx.wear.watchface.Complication? get(int id);
@@ -206,14 +206,14 @@
}
public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
}
public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
- ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList, optional int[] eglSurfaceAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis, optional int[] eglConfigAttribList);
+ ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=60000) long interactiveDrawModeUpdateDelayMillis);
method public final android.opengl.EGLConfig getEglConfig();
method public final android.opengl.EGLContext? getEglContext();
method public final android.opengl.EGLDisplay? getEglDisplay();
@@ -233,21 +233,21 @@
}
public final class WatchFace {
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
- ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.Renderer renderer);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer, optional androidx.wear.watchface.ComplicationsManager complicationsManager);
+ ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository, androidx.wear.watchface.Renderer renderer);
+ method public androidx.wear.watchface.style.CurrentUserStyleRepository getCurrentUserStyleRepository();
method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static kotlinx.coroutines.CompletableDeferred<androidx.wear.watchface.WatchFace.EditorDelegate> getOrCreateEditorDelegate(android.content.ComponentName componentName);
method public Long? getOverridePreviewReferenceTimeMillis();
- method public androidx.wear.watchface.style.UserStyleRepository getUserStyleRepository();
method public static boolean isLegacyWatchFaceOverlayStyleSupported();
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) @UiThread public static void registerEditorDelegate(android.content.ComponentName componentName, androidx.wear.watchface.WatchFace.EditorDelegate editorDelegate);
method public androidx.wear.watchface.WatchFace setLegacyWatchFaceStyle(androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle);
method public androidx.wear.watchface.WatchFace setOverridePreviewReferenceTimeMillis(@IntRange(from=0) long previewReferenceTimeMillis);
method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public androidx.wear.watchface.WatchFace setSystemTimeProvider(androidx.wear.watchface.WatchFace.SystemTimeProvider systemTimeProvider);
method public androidx.wear.watchface.WatchFace setTapListener(androidx.wear.watchface.WatchFace.TapListener? tapListener);
+ property public final androidx.wear.watchface.style.CurrentUserStyleRepository currentUserStyleRepository;
property public final androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle legacyWatchFaceStyle;
property public final Long? overridePreviewReferenceTimeMillis;
- property public final androidx.wear.watchface.style.UserStyleRepository userStyleRepository;
field public static final androidx.wear.watchface.WatchFace.Companion Companion;
}
@@ -264,8 +264,8 @@
method public androidx.wear.watchface.style.UserStyle getUserStyle();
method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
method public void onDestroy();
+ method public android.graphics.Bitmap renderWatchFaceToBitmap(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
method public void setUserStyle(androidx.wear.watchface.style.UserStyle p);
- method public android.graphics.Bitmap takeScreenshot(androidx.wear.watchface.RenderParameters renderParameters, long calendarTimeMillis, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
property public abstract androidx.wear.watchface.ComplicationsManager complicationsManager;
property public abstract long previewReferenceTimeMillis;
property public abstract android.graphics.Rect screenBounds;
diff --git a/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt b/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
index 1736917..3b2d55e 100644
--- a/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
+++ b/wear/wear-watchface/guava/src/androidTest/java/AsyncListenableWatchFaceServiceTest.kt
@@ -27,7 +27,7 @@
import androidx.wear.watchface.WatchFace
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.ListenableFuture
@@ -41,10 +41,10 @@
private class FakeRenderer(
surfaceHolder: SurfaceHolder,
watchState: WatchState,
- userStyleRepository: UserStyleRepository
+ currentUserStyleRepository: CurrentUserStyleRepository
) : Renderer.CanvasRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
CanvasType.SOFTWARE,
16
@@ -60,7 +60,7 @@
watchState: WatchState
): ListenableFuture<WatchFace> {
val future = SettableFuture.create<WatchFace>()
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(emptyList())
)
// Post a task to resolve the future.
diff --git a/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt b/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
index 272465f..489aaac 100644
--- a/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
+++ b/wear/wear-watchface/guava/src/test/java/androidx/wear/watchface/ListenableWatchFaceServiceTest.kt
@@ -20,7 +20,7 @@
import android.graphics.Rect
import android.icu.util.Calendar
import android.view.SurfaceHolder
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import com.google.common.truth.Truth.assertThat
import com.google.common.util.concurrent.Futures
@@ -42,7 +42,7 @@
watchState: WatchState
): ListenableFuture<WatchFace> {
val userStyleRepository =
- UserStyleRepository(UserStyleSchema(emptyList()))
+ CurrentUserStyleRepository(UserStyleSchema(emptyList()))
return Futures.immediateFuture(
WatchFace(
WatchFaceType.DIGITAL, userStyleRepository,
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
index 5271dda..2becac8 100644
--- a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceRenderer.java
@@ -27,7 +27,7 @@
import androidx.wear.watchface.CanvasType;
import androidx.wear.watchface.Renderer;
import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
import org.jetbrains.annotations.NotNull;
@@ -47,9 +47,9 @@
public WatchFaceRenderer(
@NotNull SurfaceHolder surfaceHolder,
- @NotNull UserStyleRepository userStyleRepository,
+ @NotNull CurrentUserStyleRepository currentUserStyleRepository,
@NotNull WatchState watchState) {
- super(surfaceHolder, userStyleRepository, watchState, CanvasType.HARDWARE,
+ super(surfaceHolder, currentUserStyleRepository, watchState, CanvasType.HARDWARE,
UPDATE_DELAY_MILLIS);
mPaint = new Paint();
mPaint.setTextAlign(Align.CENTER);
diff --git a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
index 5ceede1..5f00cde 100644
--- a/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
+++ b/wear/wear-watchface/samples/minimal/src/main/java/androidx/wear/watchface/samples/minimal/WatchFaceService.java
@@ -25,7 +25,7 @@
import androidx.wear.watchface.WatchFace;
import androidx.wear.watchface.WatchFaceType;
import androidx.wear.watchface.WatchState;
-import androidx.wear.watchface.style.UserStyleRepository;
+import androidx.wear.watchface.style.CurrentUserStyleRepository;
import androidx.wear.watchface.style.UserStyleSchema;
import androidx.wear.watchface.style.UserStyleSetting;
@@ -43,15 +43,16 @@
@Override
protected ListenableFuture<WatchFace> createWatchFaceFuture(
@NotNull SurfaceHolder surfaceHolder, @NotNull WatchState watchState) {
- UserStyleRepository userStyleRepository =
- new UserStyleRepository(
+ CurrentUserStyleRepository currentUserStyleRepository =
+ new CurrentUserStyleRepository(
new UserStyleSchema(Collections.<UserStyleSetting>emptyList()));
ComplicationsManager complicationManager =
new ComplicationsManager(Collections.<Complication>emptyList(),
- userStyleRepository);
- Renderer renderer = new WatchFaceRenderer(surfaceHolder, userStyleRepository, watchState);
+ currentUserStyleRepository);
+ Renderer renderer =
+ new WatchFaceRenderer(surfaceHolder, currentUserStyleRepository, watchState);
return Futures.immediateFuture(
- new WatchFace(WatchFaceType.DIGITAL, userStyleRepository, renderer,
+ new WatchFace(WatchFaceType.DIGITAL, currentUserStyleRepository, renderer,
complicationManager));
}
}
diff --git a/wear/wear-watchface/samples/src/main/AndroidManifest.xml b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
index 202b8d4..7112ec9 100644
--- a/wear/wear-watchface/samples/src/main/AndroidManifest.xml
+++ b/wear/wear-watchface/samples/src/main/AndroidManifest.xml
@@ -19,6 +19,7 @@
<!-- Required for complications to receive complication data and open the provider chooser. -->
<uses-feature android:name="android.hardware.type.watch" />
+ <meta-data android:name="com.google.android.wearable.standalone" android:value="true" />
<uses-permission android:name="com.google.android.permission.PROVIDE_BACKGROUND"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
@@ -44,21 +45,12 @@
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
- <intent-filter>
- <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
- <category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="text/plain" />
- </intent-filter>
-
- <meta-data
- android:name="com.google.android.wearable.standalone"
- android:value="true" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/analog_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/analog_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -78,21 +70,12 @@
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
- <intent-filter>
- <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
- <category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="text/plain" />
- </intent-filter>
-
- <meta-data
- android:name="com.google.android.wearable.standalone"
- android:value="true" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/digital_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/digital_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -112,21 +95,12 @@
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
- <intent-filter>
- <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
- <category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="text/plain" />
- </intent-filter>
-
- <meta-data
- android:name="com.google.android.wearable.standalone"
- android:value="true" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/gl_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/gl_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
@@ -146,21 +120,12 @@
<category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
</intent-filter>
- <intent-filter>
- <action android:name="com.google.android.wearable.libraries.steampack.watchface.MockTime" />
- <category android:name="android.intent.category.DEFAULT" />
- <data android:mimeType="text/plain" />
- </intent-filter>
-
- <meta-data
- android:name="com.google.android.wearable.standalone"
- android:value="true" />
<meta-data
android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/gl_background_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.preview_circular"
- android:resource="@drawable/watch_preview" />
+ android:resource="@drawable/gl_background_preview" />
<meta-data
android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR"/>
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 6ee2ef4..7f0943b 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -43,13 +43,15 @@
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import kotlin.math.cos
import kotlin.math.sin
@@ -110,57 +112,57 @@
): WatchFace {
val watchFaceStyle = WatchFaceColorStyle.create(context, RED_STYLE)
val colorStyleSetting = ListUserStyleSetting(
- COLOR_STYLE_SETTING,
+ UserStyleSetting.Id(COLOR_STYLE_SETTING),
context.getString(R.string.colors_style_setting),
context.getString(R.string.colors_style_setting_description),
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- RED_STYLE,
+ Option.Id(RED_STYLE),
context.getString(R.string.colors_style_red),
Icon.createWithResource(context, R.drawable.red_style)
),
ListUserStyleSetting.ListOption(
- GREEN_STYLE,
+ Option.Id(GREEN_STYLE),
context.getString(R.string.colors_style_green),
Icon.createWithResource(context, R.drawable.green_style)
),
ListUserStyleSetting.ListOption(
- BLUE_STYLE,
+ Option.Id(BLUE_STYLE),
context.getString(R.string.colors_style_blue),
Icon.createWithResource(context, R.drawable.blue_style)
)
),
- listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+ listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
)
val drawHourPipsStyleSetting = BooleanUserStyleSetting(
- DRAW_HOUR_PIPS_STYLE_SETTING,
+ UserStyleSetting.Id(DRAW_HOUR_PIPS_STYLE_SETTING),
context.getString(R.string.watchface_pips_setting),
context.getString(R.string.watchface_pips_setting_description),
null,
- listOf(Layer.BASE_LAYER),
+ listOf(Layer.BASE),
true
)
val watchHandLengthStyleSetting = DoubleRangeUserStyleSetting(
- WATCH_HAND_LENGTH_STYLE_SETTING,
+ UserStyleSetting.Id(WATCH_HAND_LENGTH_STYLE_SETTING),
context.getString(R.string.watchface_hand_length_setting),
context.getString(R.string.watchface_hand_length_setting_description),
null,
0.25,
1.0,
- listOf(Layer.TOP_LAYER),
+ listOf(Layer.COMPLICATIONS_OVERLAY),
0.75
)
// These are style overrides applied on top of the complications passed into
// complicationsManager below.
val complicationsStyleSetting = ComplicationsUserStyleSetting(
- COMPLICATIONS_STYLE_SETTING,
+ UserStyleSetting.Id(COMPLICATIONS_STYLE_SETTING),
context.getString(R.string.watchface_complications_setting),
context.getString(R.string.watchface_complications_setting_description),
icon = null,
complicationConfig = listOf(
ComplicationsUserStyleSetting.ComplicationsOption(
- LEFT_AND_RIGHT_COMPLICATIONS,
+ Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
context.getString(R.string.watchface_complications_setting_both),
null,
// NB this list is empty because each [ComplicationOverlay] is applied on top of
@@ -168,7 +170,7 @@
listOf()
),
ComplicationsUserStyleSetting.ComplicationsOption(
- NO_COMPLICATIONS,
+ Option.Id(NO_COMPLICATIONS),
context.getString(R.string.watchface_complications_setting_none),
null,
listOf(
@@ -183,7 +185,7 @@
)
),
ComplicationsUserStyleSetting.ComplicationsOption(
- LEFT_COMPLICATION,
+ Option.Id(LEFT_COMPLICATION),
context.getString(R.string.watchface_complications_setting_left),
null,
listOf(
@@ -194,7 +196,7 @@
)
),
ComplicationsUserStyleSetting.ComplicationsOption(
- RIGHT_COMPLICATION,
+ Option.Id(RIGHT_COMPLICATION),
context.getString(R.string.watchface_complications_setting_right),
null,
listOf(
@@ -207,7 +209,7 @@
),
listOf(Layer.COMPLICATIONS)
)
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(
listOf(
colorStyleSetting,
@@ -272,7 +274,7 @@
surfaceHolder: SurfaceHolder,
private val context: Context,
private var watchFaceColorStyle: WatchFaceColorStyle,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
private val colorStyleSetting: ListUserStyleSetting,
private val drawPipsStyleSetting: BooleanUserStyleSetting,
@@ -280,7 +282,7 @@
private val complicationsManager: ComplicationsManager
) : Renderer.CanvasRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
CanvasType.HARDWARE,
FRAME_PERIOD_MS
@@ -315,14 +317,14 @@
private var watchHandScale = 1.0f
init {
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ currentUserStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
@SuppressLint("SyntheticAccessor")
override fun onUserStyleChanged(userStyle: UserStyle) {
watchFaceColorStyle =
WatchFaceColorStyle.create(
context,
- userStyle[colorStyleSetting]!!.id
+ userStyle[colorStyleSetting]!!.toString()
)
// Apply the userStyle to the complications. ComplicationDrawables for each of
@@ -355,12 +357,12 @@
// CanvasComplicationDrawable already obeys rendererParameters.
drawComplications(canvas, calendar)
- if (renderParameters.layerParameters[Layer.TOP_LAYER] != LayerMode.HIDE) {
+ if (renderParameters.layerParameters[Layer.COMPLICATIONS_OVERLAY] != LayerMode.HIDE) {
drawClockHands(canvas, bounds, calendar, style)
}
if (renderParameters.drawMode != DrawMode.AMBIENT &&
- renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE &&
+ renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE &&
drawHourPips
) {
drawNumberStyleOuterElement(canvas, bounds, style)
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index ad7410d..35e7b94 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -56,9 +56,10 @@
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import kotlin.math.max
import kotlin.math.min
@@ -471,30 +472,30 @@
): WatchFace {
val watchFaceStyle = WatchFaceColorStyle.create(this, RED_STYLE)
val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
- COLOR_STYLE_SETTING,
+ UserStyleSetting.Id(COLOR_STYLE_SETTING),
getString(R.string.colors_style_setting),
getString(R.string.colors_style_setting_description),
icon = null,
options = listOf(
UserStyleSetting.ListUserStyleSetting.ListOption(
- RED_STYLE,
+ Option.Id(RED_STYLE),
getString(R.string.colors_style_red),
Icon.createWithResource(this, R.drawable.red_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
- GREEN_STYLE,
+ Option.Id(GREEN_STYLE),
getString(R.string.colors_style_green),
Icon.createWithResource(this, R.drawable.green_style)
),
UserStyleSetting.ListUserStyleSetting.ListOption(
- BLUE_STYLE,
+ Option.Id(BLUE_STYLE),
getString(R.string.colors_style_blue),
Icon.createWithResource(this, R.drawable.blue_style)
)
),
- listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+ listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
)
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(listOf(colorStyleSetting))
)
val leftComplication = Complication.createRoundRectComplicationBuilder(
@@ -622,7 +623,7 @@
renderer.oldBounds.set(0, 0, 0, 0)
}
return WatchFace(
- WatchFaceType.ANALOG,
+ WatchFaceType.DIGITAL,
userStyleRepository,
renderer,
complicationsManager
@@ -634,13 +635,13 @@
surfaceHolder: SurfaceHolder,
private val context: Context,
private var watchFaceColorStyle: WatchFaceColorStyle,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
private val complicationsManager: ComplicationsManager
) : Renderer.CanvasRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
CanvasType.HARDWARE,
INTERACTIVE_UPDATE_RATE_MS
@@ -649,6 +650,7 @@
private fun getBaseDigitPaint() = Paint().apply {
typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
+ isAntiAlias = true
}
private val digitTextHoursPaint = getBaseDigitPaint()
@@ -746,14 +748,14 @@
init {
// Listen for style changes.
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ currentUserStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
@SuppressLint("SyntheticAccessor")
override fun onUserStyleChanged(userStyle: UserStyle) {
watchFaceColorStyle =
WatchFaceColorStyle.create(
context,
- userStyle[colorStyleSetting]!!.id
+ userStyle[colorStyleSetting]!!.toString()
)
// Apply the userStyle to the complications. ComplicationDrawables for each of
@@ -777,6 +779,10 @@
// Trigger recomputation of bounds.
oldBounds.set(0, 0, 0, 0)
+ val antiAlias = !(it && watchState.hasLowBitAmbient)
+ digitTextHoursPaint.setAntiAlias(antiAlias)
+ digitTextMinutesPaint.setAntiAlias(antiAlias)
+ digitTextSecondsPaint.setAntiAlias(antiAlias)
}
}
@@ -821,13 +827,13 @@
applyColorStyleAndDrawMode(renderParameters.drawMode)
- if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+ if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
drawBackground(canvas)
}
drawComplications(canvas, calendar)
- if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+ if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
val is24Hour: Boolean = DateFormat.is24HourFormat(context)
nextSecondTime.timeInMillis = calendar.timeInMillis
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index c6e47e1..d229d3b 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -30,7 +30,7 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.asCoroutineDispatcher
@@ -56,7 +56,7 @@
surfaceHolder: SurfaceHolder,
watchState: WatchState
): WatchFace {
- val styleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+ val styleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
// Create the renderer on the main thread. It's EGLContext is bound to this thread.
val renderer = MainThreadRenderer(surfaceHolder, styleRepository, watchState)
@@ -154,9 +154,9 @@
internal class MainThreadRenderer(
surfaceHolder: SurfaceHolder,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState
-) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
internal var watchBodyTexture: Int = -1
internal var watchHandTexture: Int = -1
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 21a2e63..ad3fd10 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -43,9 +43,11 @@
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.style.Layer
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
@@ -92,25 +94,25 @@
): WatchFace {
val watchFaceStyle = WatchFaceColorStyle.create(context, "white_style")
val colorStyleSetting = ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization",
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- "red_style",
+ Option.Id("red_style"),
"Red",
Icon.createWithResource(context, R.drawable.red_style)
),
ListUserStyleSetting.ListOption(
- "green_style",
+ Option.Id("green_style"),
"Green",
Icon.createWithResource(context, R.drawable.green_style)
)
),
- listOf(Layer.BASE_LAYER, Layer.TOP_LAYER)
+ listOf(Layer.BASE, Layer.COMPLICATIONS_OVERLAY)
)
- val userStyleRepository = UserStyleRepository(UserStyleSchema(listOf(colorStyleSetting)))
+ val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(listOf(colorStyleSetting)))
val complicationsManager = ComplicationsManager(
listOf(
Complication.createRoundRectComplicationBuilder(
@@ -154,11 +156,11 @@
class ExampleOpenGLRenderer(
surfaceHolder: SurfaceHolder,
- private val userStyleRepository: UserStyleRepository,
+ private val currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
private val colorStyleSetting: ListUserStyleSetting,
private val complication: Complication
-) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
/** Projection transformation matrix. Converts from 3D to 2D. */
private val projectionMatrix = FloatArray(16)
@@ -576,7 +578,7 @@
GLES20.glClearColor(0f, 0f, 0f, 1f)
ambientVpMatrix
} else {
- when (userStyleRepository.userStyle[colorStyleSetting]!!.id) {
+ when (currentUserStyleRepository.userStyle[colorStyleSetting]!!.toString()) {
"red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
"green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
}
@@ -612,7 +614,7 @@
val hoursIndex = (hours / 12f * 360f).toInt()
// Render hands.
- if (renderParameters.layerParameters[Layer.TOP_LAYER] != LayerMode.HIDE) {
+ if (renderParameters.layerParameters[Layer.COMPLICATIONS_OVERLAY] != LayerMode.HIDE) {
Matrix.multiplyMM(
mvpMatrix,
0,
@@ -643,13 +645,12 @@
0
)
secondHandTriangleMap[
- userStyleRepository.userStyle[colorStyleSetting]!!.id
- ]
- ?.draw(mvpMatrix)
+ currentUserStyleRepository.userStyle[colorStyleSetting]!!.toString()
+ ]?.draw(mvpMatrix)
}
}
- if (renderParameters.layerParameters[Layer.BASE_LAYER] != LayerMode.HIDE) {
+ if (renderParameters.layerParameters[Layer.BASE] != LayerMode.HIDE) {
majorTickTriangles.draw(vpMatrix)
minorTickTriangles.draw(vpMatrix)
coloredTriangleProgram.unbindAttribs()
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 8df800e..07c4b8c 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
@@ -40,9 +40,11 @@
import androidx.wear.watchface.WatchState
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
@Sampled
fun kDocCreateExampleWatchFaceService(): WatchFaceService {
@@ -52,52 +54,52 @@
surfaceHolder: SurfaceHolder,
watchState: WatchState
): WatchFace {
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(
listOf(
ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization",
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- "red_style",
+ Option.Id("red_style"),
"Red",
icon = null
),
ListUserStyleSetting.ListOption(
- "green_style",
+ Option.Id("green_style"),
"Green",
icon = null
),
ListUserStyleSetting.ListOption(
- "bluestyle",
+ Option.Id("bluestyle"),
"Blue",
icon = null
)
),
- listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
+ listOf(Layer.BASE, Layer.COMPLICATIONS, Layer.COMPLICATIONS_OVERLAY)
),
ListUserStyleSetting(
- "hand_style_setting",
+ UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look",
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- "classic_style", "Classic", icon = null
+ Option.Id("classic_style"), "Classic", icon = null
),
ListUserStyleSetting.ListOption(
- "modern_style", "Modern", icon = null
+ Option.Id("modern_style"), "Modern", icon = null
),
ListUserStyleSetting.ListOption(
- "gothic_style",
+ Option.Id("gothic_style"),
"Gothic",
icon = null
)
),
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
)
)
@@ -150,8 +152,8 @@
/* interactiveUpdateRateMillis */ 16,
) {
init {
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ userStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
override fun onUserStyleChanged(userStyle: UserStyle) {
// `userStyle` will contain two userStyle categories with options
// from the lists above. ...
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/watch_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/analog_preview.png
similarity index 100%
rename from wear/wear-watchface/samples/src/main/res/drawable-nodpi/watch_preview.png
rename to wear/wear-watchface/samples/src/main/res/drawable-nodpi/analog_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png
new file mode 100644
index 0000000..5344bf5
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/digital_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png
new file mode 100644
index 0000000..484797d
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_background_preview.png
Binary files differ
diff --git a/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png
new file mode 100644
index 0000000..993076c
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/drawable-nodpi/gl_preview.png
Binary files differ
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index b210fe9..afdb21f 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -33,9 +33,9 @@
import androidx.wear.watchface.control.IHeadlessWatchFace
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
@@ -112,8 +112,8 @@
fun createHeadlessWatchFaceInstance() {
val instance = createInstance(100, 100)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ instance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -155,8 +155,8 @@
fun createHeadlessOpenglWatchFaceInstance() {
val instance = createOpenGlInstance(400, 400)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ instance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -189,8 +189,8 @@
fun testCommandTakeComplicationScreenShot() {
val instance = createInstance(400, 400)
val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.takeComplicationScreenshot(
- ComplicationScreenshotParams(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
RenderParameters(
DrawMode.AMBIENT,
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index ab591af..46ff4a8 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -45,14 +45,14 @@
import androidx.wear.watchface.RenderParameters
import androidx.wear.watchface.TapType
import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.InteractiveInstanceManager
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
@@ -157,7 +157,7 @@
private lateinit var canvasAnalogWatchFaceService: TestCanvasAnalogWatchFaceService
private lateinit var glesWatchFaceService: TestGlesWatchFaceService
private lateinit var engineWrapper: WatchFaceService.EngineWrapper
- private lateinit var interactiveWatchFaceInstanceWCS: IInteractiveWatchFaceWCS
+ private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
@Before
fun setUp() {
@@ -166,8 +166,8 @@
@After
fun shutDown() {
- if (this::interactiveWatchFaceInstanceWCS.isInitialized) {
- interactiveWatchFaceInstanceWCS.release()
+ if (this::interactiveWatchFaceInstance.isInitialized) {
+ interactiveWatchFaceInstance.release()
}
}
@@ -240,18 +240,18 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyleWireFormat(emptyMap()),
null
),
- object : IPendingInteractiveWatchFaceWCS.Stub() {
+ object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() =
- IPendingInteractiveWatchFaceWCS.API_VERSION
+ IPendingInteractiveWatchFace.API_VERSION
- override fun onInteractiveWatchFaceWcsCreated(
- iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
) {
- interactiveWatchFaceInstanceWCS = iInteractiveWatchFaceWcs
+ interactiveWatchFaceInstance = iInteractiveWatchFace
sendComplications()
// engineWrapper won't be initialized yet, so defer execution.
handler.post {
@@ -267,8 +267,8 @@
}
private fun sendComplications() {
- interactiveWatchFaceInstanceWCS.updateComplicationData(
- interactiveWatchFaceInstanceWCS.complicationDetails.map {
+ interactiveWatchFaceInstance.updateComplicationData(
+ interactiveWatchFaceInstance.complicationDetails.map {
IdAndComplicationDataWireFormat(
it.id,
complicationProviders[it.complicationState.fallbackSystemProvider]!!
@@ -278,13 +278,18 @@
}
private fun setAmbient(ambient: Boolean) {
- val interactiveWatchFaceInstanceSysUi =
+ val interactiveWatchFaceInstance =
InteractiveInstanceManager.getAndRetainInstance(
- interactiveWatchFaceInstanceWCS.instanceId
- )!!.createSysUiApi()
+ interactiveWatchFaceInstance.instanceId
+ )!!
- interactiveWatchFaceInstanceSysUi.setSystemState(SystemState(ambient, 0))
- interactiveWatchFaceInstanceSysUi.release()
+ interactiveWatchFaceInstance.setWatchUiState(
+ WatchUiState(
+ ambient,
+ 0
+ )
+ )
+ interactiveWatchFaceInstance.release()
}
private fun waitForPendingTaskToRunOnHandler() {
@@ -328,8 +333,8 @@
var bitmap: Bitmap? = null
handler.post {
bitmap = SharedMemoryImage.ashmemReadImageBundle(
- interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.AMBIENT,
RenderParameters.DRAW_ALL_LAYERS,
@@ -361,8 +366,8 @@
var bitmap: Bitmap? = null
handler.post {
bitmap = SharedMemoryImage.ashmemReadImageBundle(
- interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -390,9 +395,9 @@
handler.post(this::initCanvasWatchFace)
initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
handler.post {
- interactiveWatchFaceInstanceWCS.updateInstance(
+ interactiveWatchFaceInstance.updateWatchfaceInstance(
"newId",
- UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
+ UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray()))
)
sendComplications()
engineWrapper.draw()
@@ -411,14 +416,14 @@
var bitmap: Bitmap? = null
handler.post {
bitmap = SharedMemoryImage.ashmemReadImageBundle(
- interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
null,
Color.RED
@@ -448,14 +453,14 @@
var bitmap: Bitmap? = null
handler.post {
bitmap = SharedMemoryImage.ashmemReadImageBundle(
- interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
Color.RED
@@ -503,8 +508,8 @@
var bitmap: Bitmap? = null
handler.post {
bitmap = SharedMemoryImage.ashmemReadImageBundle(
- interactiveWatchFaceInstanceWCS.takeWatchFaceScreenshot(
- WatchfaceScreenshotParams(
+ interactiveWatchFaceInstance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
RenderParameters(
DrawMode.INTERACTIVE,
RenderParameters.DRAW_ALL_LAYERS,
@@ -559,9 +564,9 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyleWireFormat(
- mapOf(COLOR_STYLE_SETTING to GREEN_STYLE)
+ mapOf(COLOR_STYLE_SETTING to GREEN_STYLE.encodeToByteArray())
),
null
)
@@ -591,16 +596,16 @@
ComplicationTapActivity.newCountDown()
handler.post {
- val interactiveWatchFaceInstanceSysUi =
+ val interactiveWatchFaceInstance =
InteractiveInstanceManager.getAndRetainInstance(
- interactiveWatchFaceInstanceWCS.instanceId
- )!!.createSysUiApi()
- interactiveWatchFaceInstanceSysUi.sendTouchEvent(
+ interactiveWatchFaceInstance.instanceId
+ )!!
+ interactiveWatchFaceInstance.sendTouchEvent(
85,
165,
- TapType.TAP
+ TapType.UP
)
- interactiveWatchFaceInstanceSysUi.release()
+ interactiveWatchFaceInstance.release()
}
assertThat(ComplicationTapActivity.awaitIntent()).isNotNull()
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index 2eb3ae2..f39f8e4 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -33,7 +33,7 @@
import androidx.wear.complications.data.EmptyComplicationData
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
import java.lang.ref.WeakReference
@@ -47,12 +47,12 @@
* supported, however complications can be enabled and disabled by [ComplicationsUserStyleSetting].
*
* @param complicationCollection The complications associated with the watch face, may be empty.
- * @param userStyleRepository The [UserStyleRepository] used to listen for
+ * @param currentUserStyleRepository The [CurrentUserStyleRepository] used to listen for
* [ComplicationsUserStyleSetting] changes and apply them.
*/
public class ComplicationsManager(
complicationCollection: Collection<Complication>,
- private val userStyleRepository: UserStyleRepository
+ private val currentUserStyleRepository: CurrentUserStyleRepository
) {
/**
* Interface used to report user taps on the complication. See [addTapListener] and
@@ -100,9 +100,9 @@
@VisibleForTesting
internal constructor(
complicationCollection: Collection<Complication>,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
renderer: Renderer
- ) : this(complicationCollection, userStyleRepository) {
+ ) : this(complicationCollection, currentUserStyleRepository) {
this.renderer = renderer
}
@@ -122,7 +122,7 @@
}
val complicationsStyleCategory =
- userStyleRepository.schema.userStyleSettings.firstOrNull {
+ currentUserStyleRepository.schema.userStyleSettings.firstOrNull {
it is ComplicationsUserStyleSetting
}
@@ -131,8 +131,8 @@
if (complicationsStyleCategory != null) {
// Ensure we apply any initial StyleCategoryOption overlay by initializing with null.
var previousOption: ComplicationsUserStyleSetting.ComplicationsOption? = null
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ currentUserStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
override fun onUserStyleChanged(userStyle: UserStyle) {
val newlySelectedOption =
userStyle[complicationsStyleCategory]?.toComplicationsOption()!!
@@ -230,7 +230,7 @@
complication.id,
complication.defaultProviderPolicy.providersAsList(),
complication.defaultProviderPolicy.systemProviderFallback,
- complication.defaultProviderType.asWireComplicationType()
+ complication.defaultProviderType.toWireComplicationType()
)
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 4870e75..92fd683 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -39,7 +39,7 @@
import androidx.wear.utility.TraceEvent
import androidx.wear.watchface.Renderer.CanvasRenderer
import androidx.wear.watchface.Renderer.GlesRenderer
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import java.nio.ByteBuffer
/**
@@ -92,7 +92,7 @@
* The base class for [CanvasRenderer] and [GlesRenderer].
*
* @param surfaceHolder The [SurfaceHolder] that [renderInternal] will draw into.
- * @param userStyleRepository The associated [UserStyleRepository].
+ * @param currentUserStyleRepository The associated [CurrentUserStyleRepository].
* @param watchState The associated [WatchState].
* @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
* interactive [DrawMode]s. To render at 60hz set to 16. Note when battery is low, the frame
@@ -103,7 +103,7 @@
*/
public sealed class Renderer(
public val surfaceHolder: SurfaceHolder,
- private val userStyleRepository: UserStyleRepository,
+ private val currentUserStyleRepository: CurrentUserStyleRepository,
internal val watchState: WatchState,
@IntRange(from = 0, to = 60000)
public var interactiveDrawModeUpdateDelayMillis: Long,
@@ -169,7 +169,7 @@
/**
* Renders the watch face into the [surfaceHolder] using the current [renderParameters]
- * with the user style specified by the [userStyleRepository].
+ * with the user style specified by the [currentUserStyleRepository].
*
* @param calendar The Calendar to use when rendering the watch face
* @return A [Bitmap] containing a screenshot of the watch face
@@ -180,7 +180,7 @@
/**
* Renders the watch face into a Bitmap with the user style specified by the
- * [userStyleRepository].
+ * [currentUserStyleRepository].
*
* @param calendar The Calendar to use when rendering the watch face
* @param renderParameters The [RenderParameters] to use when rendering the watch face
@@ -265,7 +265,7 @@
*
* @param surfaceHolder The [SurfaceHolder] from which a [Canvas] to will be obtained and passed
* into [render].
- * @param userStyleRepository The watch face's associated [UserStyleRepository].
+ * @param currentUserStyleRepository The watch face's associated [CurrentUserStyleRepository].
* @param watchState The watch face's associated [WatchState].
* @param canvasType The type of canvas to request.
* @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
@@ -277,14 +277,14 @@
*/
public abstract class CanvasRenderer(
surfaceHolder: SurfaceHolder,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
@CanvasType private val canvasType: Int,
@IntRange(from = 0, to = 60000)
interactiveDrawModeUpdateDelayMillis: Long
) : Renderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
interactiveDrawModeUpdateDelayMillis
) {
@@ -364,7 +364,7 @@
*
* @param surfaceHolder The [SurfaceHolder] whose [android.view.Surface] [render] will draw
* into.
- * @param userStyleRepository The associated [UserStyleRepository].
+ * @param currentUserStyleRepository The associated [CurrentUserStyleRepository].
* @param watchState The associated [WatchState].
* @param interactiveDrawModeUpdateDelayMillis The interval in milliseconds between frames in
* interactive [DrawMode]s. To render at 60hz set to 16. Note when battery is low, the
@@ -379,7 +379,7 @@
*/
public abstract class GlesRenderer @JvmOverloads constructor(
surfaceHolder: SurfaceHolder,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
@IntRange(from = 0, to = 60000)
interactiveDrawModeUpdateDelayMillis: Long,
@@ -387,7 +387,7 @@
private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
) : Renderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
interactiveDrawModeUpdateDelayMillis
) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index a26ac652..e541719 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -31,6 +31,7 @@
import android.os.BatteryManager
import android.os.Build
import android.support.wearable.watchface.WatchFaceStyle
+import android.util.Base64
import android.view.Surface.FRAME_RATE_COMPATIBILITY_DEFAULT
import android.view.ViewConfiguration
import androidx.annotation.ColorInt
@@ -45,8 +46,9 @@
import androidx.wear.complications.SystemProviders
import androidx.wear.complications.data.ComplicationData
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.data.UserStyleWireFormat
import kotlinx.coroutines.CompletableDeferred
import java.io.FileNotFoundException
@@ -84,13 +86,13 @@
}
private fun readPrefs(context: Context, fileName: String): UserStyleWireFormat {
- val hashMap = HashMap<String, String>()
+ val hashMap = HashMap<String, ByteArray>()
try {
val reader = InputStreamReader(context.openFileInput(fileName)).buffered()
while (true) {
val key = reader.readLine() ?: break
val value = reader.readLine() ?: break
- hashMap[key] = value
+ hashMap[key] = Base64.decode(value, Base64.NO_WRAP)
}
reader.close()
} catch (e: FileNotFoundException) {
@@ -102,9 +104,9 @@
private fun writePrefs(context: Context, fileName: String, style: UserStyle) {
val writer = context.openFileOutput(fileName, Context.MODE_PRIVATE).bufferedWriter()
for ((key, value) in style.selectedOptions) {
- writer.write(key.id)
+ writer.write(key.id.value)
writer.newLine()
- writer.write(value.id)
+ writer.write(Base64.encodeToString(value.id.value, Base64.NO_WRAP))
writer.newLine()
}
writer.close()
@@ -116,16 +118,16 @@
*
* @param watchFaceType The type of watch face, whether it's digital or analog. Used to determine
* the default time for editor preview screenshots.
- * @param userStyleRepository The [UserStyleRepository] for this WatchFace.
+ * @param currentUserStyleRepository The [CurrentUserStyleRepository] for this WatchFace.
* @param renderer The [Renderer] for this WatchFace.
* @param complicationsManager The [ComplicationsManager] for this WatchFace.
*/
public class WatchFace @JvmOverloads constructor(
@WatchFaceType internal var watchFaceType: Int,
- public val userStyleRepository: UserStyleRepository,
+ public val currentUserStyleRepository: CurrentUserStyleRepository,
internal val renderer: Renderer,
internal var complicationsManager: ComplicationsManager =
- ComplicationsManager(emptyList(), userStyleRepository)
+ ComplicationsManager(emptyList(), currentUserStyleRepository)
) {
internal var tapListener: TapListener? = null
@@ -220,8 +222,8 @@
/** The UTC reference time to use for previews in milliseconds since the epoch. */
public val previewReferenceTimeMillis: Long
- /** Takes a screenshot with the [UserStyleRepository]'s [UserStyle]. */
- public fun takeScreenshot(
+ /** Renders the watchface to a [Bitmap] with the [CurrentUserStyleRepository]'s [UserStyle]. */
+ public fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
@@ -266,9 +268,9 @@
* only the vertical gravity is respected.
* @param tapEventsAccepted Controls whether this watch face accepts tap events. Watchfaces
* that set this {@code true} are indicating they are prepared to receive
- * [IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH],
- * [IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL], and
- * [IInteractiveWatchFaceSysUI.TAP_TYPE_TAP] events.
+ * [IInteractiveWatchFaceSysUI.TAP_TYPE_DOWN],
+ * [IInteractiveWatchFaceSysUI.TAP_TYPE_CANCEL], and
+ * [IInteractiveWatchFaceSysUI.TAP_TYPE_UP] events.
* @param accentColor The accent color which will be used when drawing the unread notification
* indicator. Default color is white.
* @throws IllegalArgumentException if [viewProtectionMode] has an unexpected value
@@ -392,7 +394,7 @@
private val systemTimeProvider = watchface.systemTimeProvider
private val legacyWatchFaceStyle = watchface.legacyWatchFaceStyle
- internal val userStyleRepository = watchface.userStyleRepository
+ internal val userStyleRepository = watchface.currentUserStyleRepository
internal val renderer = watchface.renderer
internal val complicationsManager = watchface.complicationsManager
private val tapListener = watchface.tapListener
@@ -508,19 +510,19 @@
val storedUserStyle = watchFaceHostApi.getInitialUserStyle()
if (storedUserStyle != null) {
userStyleRepository.userStyle =
- UserStyle(storedUserStyle, userStyleRepository.schema)
+ UserStyle(UserStyleData(storedUserStyle), userStyleRepository.schema)
} else {
// The system doesn't support preference persistence we need to do it ourselves.
val preferencesFile =
"watchface_prefs_${watchFaceHostApi.getContext().javaClass.name}.txt"
userStyleRepository.userStyle = UserStyle(
- readPrefs(watchFaceHostApi.getContext(), preferencesFile),
+ UserStyleData(readPrefs(watchFaceHostApi.getContext(), preferencesFile)),
userStyleRepository.schema
)
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ userStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
@SuppressLint("SyntheticAccessor")
override fun onUserStyleChanged(userStyle: UserStyle) {
writePrefs(watchFaceHostApi.getContext(), preferencesFile, userStyle)
@@ -647,7 +649,7 @@
override val previewReferenceTimeMillis
get() = [email protected]
- override fun takeScreenshot(
+ override fun renderWatchFaceToBitmap(
renderParameters: RenderParameters,
calendarTimeMillis: Long,
idToComplicationData: Map<Int, ComplicationData>?
@@ -899,13 +901,13 @@
// TODO(alexclarke): Revisit this
var tapType = originalTapType
when (tapType) {
- TapType.TOUCH -> {
+ TapType.DOWN -> {
lastTappedPosition = Point(x, y)
}
- TapType.TOUCH_CANCEL -> {
+ TapType.CANCEL -> {
lastTappedPosition?.let { safeLastTappedPosition ->
if ((safeLastTappedPosition.x == x) && (safeLastTappedPosition.y == y)) {
- tapType = TapType.TAP
+ tapType = TapType.UP
}
}
lastTappedPosition = null
@@ -913,7 +915,7 @@
}
when (tapType) {
- TapType.TAP -> {
+ TapType.UP -> {
if (tappedComplication.id != lastTappedComplicationId &&
lastTappedComplicationId != null
) {
@@ -938,7 +940,7 @@
}
}
}
- TapType.TOUCH -> {
+ TapType.DOWN -> {
// Make sure the user isn't doing a swipe.
if (tappedComplication.id != lastTappedComplicationId &&
lastTappedComplicationId != null
@@ -970,8 +972,8 @@
writer.println("pendingUpdateTime=${pendingUpdateTime.isPending()}")
writer.println("lastTappedComplicationId=$lastTappedComplicationId")
writer.println("lastTappedPosition=$lastTappedPosition")
- writer.println("userStyleRepository.userStyle=${userStyleRepository.userStyle}")
- writer.println("userStyleRepository.schema=${userStyleRepository.schema}")
+ writer.println("currentUserStyleRepository.userStyle=${userStyleRepository.userStyle}")
+ writer.println("currentUserStyleRepository.schema=${userStyleRepository.schema}")
watchState.dump(writer)
complicationsManager.dump(writer)
renderer.dump(writer)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 6df1310..61b0832 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -49,29 +49,31 @@
import androidx.wear.complications.SystemProviders.ProviderId
import androidx.wear.complications.data.ComplicationData
import androidx.wear.complications.data.ComplicationType
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.utility.AsyncTraceEvent
import androidx.wear.utility.TraceEvent
import androidx.wear.watchface.control.HeadlessWatchFaceImpl
-import androidx.wear.watchface.control.IInteractiveWatchFaceSysUI
+import androidx.wear.watchface.control.IInteractiveWatchFace
import androidx.wear.watchface.control.InteractiveInstanceManager
import androidx.wear.watchface.control.InteractiveWatchFaceImpl
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
import androidx.wear.watchface.control.data.HeadlessWatchFaceInstanceParams
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.ComplicationStateWireFormat
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
import androidx.wear.watchface.data.IdAndComplicationStateWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.editor.EditorService
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSetting
+import androidx.wear.watchface.style.UserStyleData
import androidx.wear.watchface.style.data.UserStyleWireFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.io.FileDescriptor
import java.io.FileNotFoundException
@@ -93,29 +95,31 @@
/** @hide */
@IntDef(
value = [
- TapType.TOUCH,
- TapType.TOUCH_CANCEL,
- TapType.TAP
+ TapType.DOWN,
+ TapType.UP,
+ TapType.CANCEL
]
)
public annotation class TapType {
public companion object {
- /** Used in [WatchFaceImpl#onTapCommand] to indicate a "down" touch event on the watch face. */
- public const val TOUCH: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH
+ /**
+ * Used in [WatchFaceImpl#onTapCommand] to indicate a "down" touch event on the watch face.
+ */
+ public const val DOWN: Int = IInteractiveWatchFace.TAP_TYPE_DOWN
/**
- * Used in [WatchFaceImpl#onTapCommand] to indicate that a previous TapType.TOUCH touch event
- * has been canceled. This generally happens when the watch face is touched but then a
+ * Used in [WatchFaceImpl#onTapCommand] to indicate that a previous [TapType.DOWN] touch
+ * event has been canceled. This generally happens when the watch face is touched but then a
* move or long press occurs.
*/
- public const val TOUCH_CANCEL: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TOUCH_CANCEL
+ public const val CANCEL: Int = IInteractiveWatchFace.TAP_TYPE_CANCEL
/**
* Used in [WatchFaceImpl#onTapCommand] to indicate that an "up" event on the watch face has
- * occurred that has not been consumed by another activity. A TapType.TOUCH will always
- * occur first. This event will not occur if a TapType.TOUCH_CANCEL is sent.
+ * occurred that has not been consumed by another activity. A [TapType.DOWN] will always
+ * occur first. This event will not occur if a [TapType.CANCEL] is sent.
*/
- public const val TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
+ public const val UP: Int = IInteractiveWatchFace.TAP_TYPE_UP
}
}
@@ -136,7 +140,7 @@
*
* Watch face styling (color and visual look of watch face elements such as numeric fonts, watch
* hands and ticks, etc...) is directly supported via [UserStyleSetting] and
- * [UserStyleRepository].
+ * [CurrentUserStyleRepository].
*
* To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down
* time, and to loop between two instants. This is controlled by MOCK_TIME_INTENT intents
@@ -208,7 +212,10 @@
/** Whether to log every frame. */
private const val LOG_VERBOSE = false
- /** Whether to enable tracing for each call to [Engine.onDraw]. */
+ /**
+ * Whether to enable tracing for each call to [WatchFaceImpl.onDraw()] and
+ * [WatchFaceImpl.onSurfaceRedrawNeeded()]
+ */
private const val TRACE_DRAW = false
// Reference time for editor screenshots for analog watch faces.
@@ -366,7 +373,7 @@
// Only valid after onSetBinder has been called.
private var systemApiVersion = -1
- internal var firstSetSystemState = true
+ internal var firstSetWatchUiState = true
internal var immutableSystemStateDone = false
private var ignoreNextOnVisibilityChanged = false
@@ -403,7 +410,7 @@
createInteractiveInstance(
directBootParams!!,
"DirectBoot"
- ).createWCSApi()
+ )
asyncTraceEvent.close()
}
}
@@ -421,11 +428,11 @@
// workaround the workaround...
ignoreNextOnVisibilityChanged = true
coroutineScope.launch {
- pendingWallpaperInstance.callback.onInteractiveWatchFaceWcsCreated(
+ pendingWallpaperInstance.callback.onInteractiveWatchFaceCreated(
createInteractiveInstance(
pendingWallpaperInstance.params,
"Boot with pendingWallpaperInstance"
- ).createWCSApi()
+ )
)
asyncTraceEvent.close()
val params = pendingWallpaperInstance.params
@@ -475,20 +482,20 @@
}
@UiThread
- fun setSystemState(systemState: SystemState) {
- if (firstSetSystemState ||
- systemState.inAmbientMode != mutableWatchState.isAmbient.value
+ fun setWatchUiState(watchUiState: WatchUiState) {
+ if (firstSetWatchUiState ||
+ watchUiState.inAmbientMode != mutableWatchState.isAmbient.value
) {
- mutableWatchState.isAmbient.value = systemState.inAmbientMode
+ mutableWatchState.isAmbient.value = watchUiState.inAmbientMode
}
- if (firstSetSystemState ||
- systemState.interruptionFilter != mutableWatchState.interruptionFilter.value
+ if (firstSetWatchUiState ||
+ watchUiState.interruptionFilter != mutableWatchState.interruptionFilter.value
) {
- mutableWatchState.interruptionFilter.value = systemState.interruptionFilter
+ mutableWatchState.interruptionFilter.value = watchUiState.interruptionFilter
}
- firstSetSystemState = false
+ firstSetWatchUiState = false
}
@UiThread
@@ -496,7 +503,7 @@
userStyle: UserStyleWireFormat
): Unit = TraceEvent("EngineWrapper.setUserStyle").use {
watchFaceImpl.onSetStyleInternal(
- UserStyle(userStyle, watchFaceImpl.userStyleRepository.schema)
+ UserStyle(UserStyleData(userStyle), watchFaceImpl.userStyleRepository.schema)
)
onUserStyleChanged()
}
@@ -550,11 +557,11 @@
ComplicationType.toWireTypes(it.value.supportedTypes),
it.value.defaultProviderPolicy.providersAsList(),
it.value.defaultProviderPolicy.systemProviderFallback,
- it.value.defaultProviderType.asWireComplicationType(),
+ it.value.defaultProviderType.toWireComplicationType(),
it.value.enabled,
it.value.initiallyEnabled,
- it.value.renderer.getData()?.type?.asWireComplicationType()
- ?: ComplicationType.NO_DATA.asWireComplicationType(),
+ it.value.renderer.getData()?.type?.toWireComplicationType()
+ ?: ComplicationType.NO_DATA.toWireComplicationType(),
it.value.fixedComplicationProvider,
it.value.configExtras
)
@@ -570,7 +577,7 @@
for (idAndComplicationData in complicationDatumWireFormats) {
watchFaceImpl.onComplicationDataUpdate(
idAndComplicationData.id,
- idAndComplicationData.complicationData.asApiComplicationData()
+ idAndComplicationData.complicationData.toApiComplicationData()
)
}
} else {
@@ -578,7 +585,7 @@
pendingComplicationDataUpdates.add(
PendingComplicationData(
idAndComplicationData.id,
- idAndComplicationData.complicationData.asApiComplicationData()
+ idAndComplicationData.complicationData.toApiComplicationData()
)
)
}
@@ -605,13 +612,13 @@
@UiThread
@RequiresApi(27)
- fun takeWatchFaceScreenshot(
- params: WatchfaceScreenshotParams
- ): Bundle = TraceEvent("EngineWrapper.takeWatchFaceScreenshot").use {
+ fun renderWatchFaceToBitmap(
+ params: WatchFaceRenderParams
+ ): Bundle = TraceEvent("EngineWrapper.renderWatchFaceToBitmap").use {
val oldStyle = HashMap(watchFaceImpl.userStyleRepository.userStyle.selectedOptions)
params.userStyle?.let {
watchFaceImpl.onSetStyleInternal(
- UserStyle(it, watchFaceImpl.userStyleRepository.schema)
+ UserStyle(UserStyleData(it), watchFaceImpl.userStyleRepository.schema)
)
}
@@ -624,7 +631,7 @@
params.idAndComplicationDatumWireFormats?.let {
for (idAndData in it) {
watchFaceImpl.complicationsManager[idAndData.id]!!.renderer
- .loadData(idAndData.complicationData.asApiComplicationData(), false)
+ .loadData(idAndData.complicationData.toApiComplicationData(), false)
}
}
@@ -651,9 +658,9 @@
@UiThread
@RequiresApi(27)
- fun takeComplicationScreenshot(
- params: ComplicationScreenshotParams
- ): Bundle? = TraceEvent("EngineWrapper.takeComplicationScreenshot").use {
+ fun renderComplicationToBitmap(
+ params: ComplicationRenderParams
+ ): Bundle? = TraceEvent("EngineWrapper.renderComplicationToBitmap").use {
val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
timeInMillis = params.calendarTimeMillis
}
@@ -662,7 +669,7 @@
val newStyle = params.userStyle
if (newStyle != null) {
watchFaceImpl.onSetStyleInternal(
- UserStyle(newStyle, watchFaceImpl.userStyleRepository.schema)
+ UserStyle(UserStyleData(newStyle), watchFaceImpl.userStyleRepository.schema)
)
}
@@ -678,7 +685,7 @@
if (screenshotComplicationData != null) {
prevData = it.renderer.getData()
it.renderer.loadData(
- screenshotComplicationData.asApiComplicationData(),
+ screenshotComplicationData.toApiComplicationData(),
false
)
}
@@ -771,6 +778,7 @@
override fun onDestroy(): Unit = TraceEvent("EngineWrapper.onDestroy").use {
destroyed = true
+ coroutineScope.cancel()
uiThreadHandler.removeCallbacks(invalidateRunnable)
if (this::choreographer.isInitialized) {
choreographer.removeFrameCallback(frameCallback)
@@ -827,15 +835,15 @@
}
Constants.COMMAND_TAP ->
uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TAP") {
- sendTouchEvent(x, y, TapType.TAP)
+ sendTouchEvent(x, y, TapType.UP)
}
Constants.COMMAND_TOUCH ->
uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TOUCH") {
- sendTouchEvent(x, y, TapType.TOUCH)
+ sendTouchEvent(x, y, TapType.DOWN)
}
Constants.COMMAND_TOUCH_CANCEL ->
uiThreadHandler.runOnHandlerWithTracing("onCommand COMMAND_TOUCH_CANCEL") {
- sendTouchEvent(x, y, TapType.TOUCH_CANCEL)
+ sendTouchEvent(x, y, TapType.CANCEL)
}
else -> {
}
@@ -852,8 +860,8 @@
return
}
- setSystemState(
- SystemState(
+ setWatchUiState(
+ WatchUiState(
extras.getBoolean(
Constants.EXTRA_AMBIENT_MODE,
mutableWatchState.isAmbient.getValueOr(false)
@@ -983,7 +991,7 @@
require(!mutableWatchState.isHeadless)
setImmutableSystemState(params.deviceConfig)
- setSystemState(params.systemState)
+ setWatchUiState(params.watchUiState)
initialUserStyle = params.userStyle
val watchState = mutableWatchState.asWatchState()
@@ -1015,7 +1023,7 @@
"Miss match between pendingWallpaperInstance id $it.params.instanceId and " +
"constructed instance id $params.instanceId"
}
- it.callback.onInteractiveWatchFaceWcsCreated(instance.createWCSApi())
+ it.callback.onInteractiveWatchFaceCreated(instance)
}
return instance
@@ -1178,7 +1186,7 @@
extras.getParcelable(Constants.EXTRA_COMPLICATION_DATA)!!
setComplicationData(
extras.getInt(Constants.EXTRA_COMPLICATION_ID),
- complicationData.asApiComplicationData()
+ complicationData.toApiComplicationData()
)
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
index 3fb6e55..effcd4a 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/HeadlessWatchFaceImpl.kt
@@ -21,8 +21,8 @@
import androidx.annotation.UiThread
import androidx.wear.watchface.IndentingPrintWriter
import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.data.ComplicationScreenshotParams
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.ComplicationRenderParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.runOnHandlerWithTracing
/**
@@ -63,9 +63,9 @@
override fun getApiVersion() = IHeadlessWatchFace.API_VERSION
- override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
- uiThreadHandler.runOnHandlerWithTracing("HeadlessWatchFaceImpl.takeWatchFaceScreenshot") {
- engine!!.takeWatchFaceScreenshot(params)
+ override fun renderWatchFaceToBitmap(params: WatchFaceRenderParams) =
+ uiThreadHandler.runOnHandlerWithTracing("HeadlessWatchFaceImpl.renderWatchFaceToBitmap") {
+ engine!!.renderWatchFaceToBitmap(params)
}
override fun getPreviewReferenceTimeMillis() = engine!!.watchFaceImpl.previewReferenceTimeMillis
@@ -75,11 +75,11 @@
engine!!.getComplicationState()
}
- override fun takeComplicationScreenshot(params: ComplicationScreenshotParams) =
+ override fun renderComplicationToBitmap(params: ComplicationRenderParams) =
uiThreadHandler.runOnHandlerWithTracing(
- "HeadlessWatchFaceImpl.takeComplicationScreenshot"
+ "HeadlessWatchFaceImpl.renderComplicationToBitmap"
) {
- engine!!.takeComplicationScreenshot(params)
+ engine!!.renderComplicationToBitmap(params)
}
override fun getUserStyleSchema() =
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
index 6f759ee..0fe64b8 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
@@ -43,7 +43,7 @@
class PendingWallpaperInteractiveWatchFaceInstance(
val params: WallpaperInteractiveWatchFaceInstanceParams,
- val callback: IPendingInteractiveWatchFaceWCS
+ val callback: IPendingInteractiveWatchFace
)
companion object {
@@ -108,11 +108,11 @@
@RequiresApi(27)
fun getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
value: PendingWallpaperInteractiveWatchFaceInstance
- ): IInteractiveWatchFaceWCS? {
+ ): IInteractiveWatchFace? {
synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
val instance = instances[value.params.instanceId]
return if (instance != null) {
- instance.impl.createWCSApi()
+ instance.impl
} else {
pendingWallpaperInteractiveWatchFaceInstance = value
null
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
index 0d70ea3..4f537a63 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/InteractiveWatchFaceImpl.kt
@@ -20,9 +20,9 @@
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import androidx.annotation.RequiresApi
import androidx.wear.watchface.WatchFaceService
-import androidx.wear.watchface.control.data.WatchfaceScreenshotParams
+import androidx.wear.watchface.control.data.WatchFaceRenderParams
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.runOnHandlerWithTracing
import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -32,118 +32,88 @@
internal val engine: WatchFaceService.EngineWrapper,
internal var instanceId: String,
private val uiThreadHandler: Handler
-) {
- fun createSysUiApi() = SysUiApi(engine, this, uiThreadHandler)
+) : IInteractiveWatchFace.Stub() {
- fun createWCSApi() = WCSApi(engine, this, uiThreadHandler)
-}
-
-/** The interface for SysUI. */
-@RequiresApi(27)
-internal class SysUiApi(
- private val engine: WatchFaceService.EngineWrapper,
- private val instance: InteractiveWatchFaceImpl,
- private val uiThreadHandler: Handler
-) : IInteractiveWatchFaceSysUI.Stub() {
- override fun getApiVersion() = IInteractiveWatchFaceSysUI.API_VERSION
+ override fun getApiVersion() = IInteractiveWatchFace.API_VERSION
override fun sendTouchEvent(xPos: Int, yPos: Int, tapType: Int) {
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.sendTouchEvent") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.sendTouchEvent") {
engine.sendTouchEvent(xPos, yPos, tapType)
}
}
override fun getContentDescriptionLabels(): Array<ContentDescriptionLabel> =
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.getContentDescriptionLabels") {
+ uiThreadHandler.runOnHandlerWithTracing(
+ "InteractiveWatchFaceImpl.getContentDescriptionLabels"
+ ) {
engine.watchFaceImpl.complicationsManager.getContentDescriptionLabels()
}
- override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.takeWatchFaceScreenshot") {
- engine.takeWatchFaceScreenshot(params)
+ override fun renderWatchFaceToBitmap(params: WatchFaceRenderParams) =
+ uiThreadHandler.runOnHandlerWithTracing(
+ "InteractiveWatchFaceImpl.renderWatchFaceToBitmap"
+ ) {
+ engine.renderWatchFaceToBitmap(params)
}
override fun getPreviewReferenceTimeMillis() = engine.watchFaceImpl.previewReferenceTimeMillis
- override fun setSystemState(systemState: SystemState) {
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.setSystemState") {
- engine.setSystemState(systemState)
+ override fun setWatchUiState(watchUiState: WatchUiState) {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.setSystemState") {
+ engine.setWatchUiState(watchUiState)
}
}
- override fun getInstanceId(): String = instance.instanceId
+ override fun getInstanceId(): String = instanceId
override fun ambientTickUpdate() {
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.ambientTickUpdate") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.ambientTickUpdate") {
engine.ambientTickUpdate()
}
}
override fun release() {
- uiThreadHandler.runOnHandlerWithTracing("SysUiApi.release") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.release") {
InteractiveInstanceManager.releaseInstance(instanceId)
}
}
-}
-
-/** The interface for WCS. */
-@RequiresApi(27)
-internal class WCSApi(
- private val engine: WatchFaceService.EngineWrapper,
- private val instance: InteractiveWatchFaceImpl,
- private val uiThreadHandler: Handler
-) : IInteractiveWatchFaceWCS.Stub() {
- override fun getApiVersion() = IInteractiveWatchFaceWCS.API_VERSION
override fun updateComplicationData(
complicationDatumWireFormats: MutableList<IdAndComplicationDataWireFormat>
) {
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.updateComplicationData") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.updateComplicationData") {
engine.setComplicationDataList(complicationDatumWireFormats)
}
}
- override fun takeWatchFaceScreenshot(params: WatchfaceScreenshotParams) =
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.takeWatchFaceScreenshot") {
- engine.takeWatchFaceScreenshot(params)
- }
-
- override fun getPreviewReferenceTimeMillis() = engine.watchFaceImpl.previewReferenceTimeMillis
-
- override fun updateInstance(
+ override fun updateWatchfaceInstance(
newInstanceId: String,
userStyle: UserStyleWireFormat
) {
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.updateInstance") {
- if (instance.instanceId != newInstanceId) {
- InteractiveInstanceManager.renameInstance(instance.instanceId, newInstanceId)
- instance.instanceId = newInstanceId
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.updateInstance") {
+ if (instanceId != newInstanceId) {
+ InteractiveInstanceManager.renameInstance(instanceId, newInstanceId)
+ instanceId = newInstanceId
}
engine.setUserStyle(userStyle)
engine.clearComplicationData()
}
}
- override fun getInstanceId(): String = instance.instanceId
-
- override fun release() {
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.release") {
- InteractiveInstanceManager.releaseInstance(instanceId)
- }
- }
-
override fun getComplicationDetails() =
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.getComplicationDetails") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.getComplicationDetails") {
engine.getComplicationState()
}
override fun getUserStyleSchema() =
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.getUserStyleSchema") {
+ uiThreadHandler.runOnHandlerWithTracing("InteractiveWatchFaceImpl.getUserStyleSchema") {
engine.watchFaceImpl.userStyleRepository.schema.toWireFormat()
}
override fun bringAttentionToComplication(id: Int) {
- uiThreadHandler.runOnHandlerWithTracing("WCSApi.bringAttentionToComplication") {
+ uiThreadHandler.runOnHandlerWithTracing(
+ "InteractiveWatchFaceImpl.bringAttentionToComplication"
+ ) {
engine.watchFaceImpl.complicationsManager.displayPressedAnimation(id)
}
}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index 5a07abc..d0974e9 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -103,10 +103,10 @@
) : IWatchFaceControlService.Stub() {
override fun getApiVersion() = IWatchFaceControlService.API_VERSION
- override fun getInteractiveWatchFaceInstanceSysUI(instanceId: String) =
- TraceEvent("IWatchFaceInstanceServiceStub.getInteractiveWatchFaceInstanceSysUI").use {
+ override fun getInteractiveWatchFaceInstance(instanceId: String) =
+ TraceEvent("IWatchFaceInstanceServiceStub.getInteractiveWatchFaceInstance").use {
// This call is thread safe so we don't need to trampoline via the UI thread.
- InteractiveInstanceManager.getAndRetainInstance(instanceId)?.createSysUiApi()
+ InteractiveInstanceManager.getAndRetainInstance(instanceId)
}
override fun createHeadlessWatchFaceInstance(
@@ -144,25 +144,25 @@
}
}
- override fun getOrCreateInteractiveWatchFaceWCS(
+ override fun getOrCreateInteractiveWatchFace(
params: WallpaperInteractiveWatchFaceInstanceParams,
- callback: IPendingInteractiveWatchFaceWCS
- ): IInteractiveWatchFaceWCS? {
+ callback: IPendingInteractiveWatchFace
+ ): IInteractiveWatchFace? {
val asyncTraceEvent =
AsyncTraceEvent("IWatchFaceInstanceServiceStub.getOrCreateInteractiveWatchFaceWCS")
return InteractiveInstanceManager
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
params,
- // Wrapped IPendingInteractiveWatchFaceWCS to support tracing.
- object : IPendingInteractiveWatchFaceWCS.Stub() {
+ // Wrapped IPendingInteractiveWatchFace to support tracing.
+ object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() = callback.apiVersion
- override fun onInteractiveWatchFaceWcsCreated(
- iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS?
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFaceWcs: IInteractiveWatchFace?
) {
asyncTraceEvent.close()
- callback.onInteractiveWatchFaceWcsCreated(iInteractiveWatchFaceWcs)
+ callback.onInteractiveWatchFaceCreated(iInteractiveWatchFaceWcs)
}
}
)
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index 719ede7..2583b43 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -21,14 +21,14 @@
import android.os.Looper
import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.InteractiveInstanceManager
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.DeviceConfig
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
@@ -94,7 +94,7 @@
private val surfaceHolder = mock<SurfaceHolder>()
private var looperTimeMillis = 0L
private val pendingTasks = PriorityQueue<Task>()
- private val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+ private val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
private val initParams = WallpaperInteractiveWatchFaceInstanceParams(
"instanceId",
DeviceConfig(
@@ -103,7 +103,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
@@ -221,7 +221,7 @@
engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
runPostedTasksFor(0)
- var pendingInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS? = null
+ var pendingInteractiveWatchFaceWcs: IInteractiveWatchFace? = null
// There shouldn't be an existing instance, so we expect null.
assertNull(
@@ -229,12 +229,12 @@
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
initParams,
- object : IPendingInteractiveWatchFaceWCS.Stub() {
+ object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() =
- IPendingInteractiveWatchFaceWCS.API_VERSION
+ IPendingInteractiveWatchFace.API_VERSION
- override fun onInteractiveWatchFaceWcsCreated(
- iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS?
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFaceWcs: IInteractiveWatchFace?
) {
pendingInteractiveWatchFaceWcs = iInteractiveWatchFaceWcs
}
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index 5796be5..15e579e 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -31,10 +31,10 @@
import android.support.wearable.watchface.accessibility.ContentDescriptionLabel
import android.view.SurfaceHolder
import androidx.test.core.app.ApplicationProvider
-import androidx.wear.complications.data.asApiComplicationData
+import androidx.wear.complications.data.toApiComplicationData
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import org.junit.runners.model.FrameworkMethod
import org.robolectric.RobolectricTestRunner
import org.robolectric.internal.bytecode.InstrumentationConfiguration
@@ -43,7 +43,7 @@
@WatchFaceType private val watchFaceType: Int,
private val complicationsManager: ComplicationsManager,
private val renderer: TestRenderer,
- private val userStyleRepository: UserStyleRepository,
+ private val currentUserStyleRepository: CurrentUserStyleRepository,
private val watchState: MutableWatchState,
private val handler: Handler,
private val tapListener: WatchFace.TapListener?,
@@ -57,8 +57,8 @@
var lastUserStyle: UserStyle? = null
init {
- userStyleRepository.addUserStyleListener(
- object : UserStyleRepository.UserStyleListener {
+ currentUserStyleRepository.addUserStyleChangeListener(
+ object : CurrentUserStyleRepository.UserStyleChangeListener {
override fun onUserStyleChanged(userStyle: UserStyle) {
lastUserStyle = userStyle
}
@@ -94,7 +94,7 @@
watchState: WatchState
) = WatchFace(
watchFaceType,
- userStyleRepository,
+ currentUserStyleRepository,
renderer,
complicationsManager
).setSystemTimeProvider(object : WatchFace.SystemTimeProvider {
@@ -179,16 +179,20 @@
watchFaceComplicationId, providers, fallbackSystemProvider, type
)
}
+
+ override fun reserved8() {
+ iWatchFaceService.reserved8()
+ }
}
open class TestRenderer(
surfaceHolder: SurfaceHolder,
- userStyleRepository: UserStyleRepository,
+ currentUserStyleRepository: CurrentUserStyleRepository,
watchState: WatchState,
interactiveFrameRateMs: Long
) : Renderer.CanvasRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
CanvasType.HARDWARE,
interactiveFrameRateMs
@@ -214,7 +218,7 @@
ApplicationProvider.getApplicationContext(), 0,
Intent("Fake intent"), 0
)
- ).build().asApiComplicationData()
+ ).build().toApiComplicationData()
/**
* We need to prevent roboloetric from instrumenting our classes or things break...
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 ad7f0a6..98fa565 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
@@ -42,22 +42,24 @@
import androidx.wear.complications.data.ComplicationType
import androidx.wear.complications.data.ShortTextComplicationData
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
-import androidx.wear.watchface.control.IPendingInteractiveWatchFaceWCS
+import androidx.wear.watchface.control.IInteractiveWatchFace
+import androidx.wear.watchface.control.IPendingInteractiveWatchFace
import androidx.wear.watchface.control.InteractiveInstanceManager
import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
import androidx.wear.watchface.data.ComplicationBoundsType
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
-import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.data.WatchUiState
import androidx.wear.watchface.style.Layer
import androidx.wear.watchface.style.UserStyle
-import androidx.wear.watchface.style.UserStyleRepository
+import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationOverlay
import androidx.wear.watchface.style.UserStyleSetting.ComplicationsUserStyleSetting.ComplicationsOption
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.Option
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
import org.junit.After
@@ -115,48 +117,48 @@
private val complicationDrawableBackground = ComplicationDrawable(context)
private val redStyleOption =
- ListUserStyleSetting.ListOption("red_style", "Red", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
private val greenStyleOption =
- ListUserStyleSetting.ListOption("green_style", "Green", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("green_style"), "Green", icon = null)
private val blueStyleOption =
- ListUserStyleSetting.ListOption("bluestyle", "Blue", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("bluestyle"), "Blue", icon = null)
private val colorStyleList = listOf(redStyleOption, greenStyleOption, blueStyleOption)
private val colorStyleSetting = ListUserStyleSetting(
- "color_style_setting",
+ UserStyleSetting.Id("color_style_setting"),
"Colors",
"Watchface colorization", /* icon = */
null,
colorStyleList,
- listOf(Layer.BASE_LAYER)
+ listOf(Layer.BASE)
)
private val classicStyleOption =
- ListUserStyleSetting.ListOption("classic_style", "Classic", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("classic_style"), "Classic", icon = null)
private val modernStyleOption =
- ListUserStyleSetting.ListOption("modern_style", "Modern", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("modern_style"), "Modern", icon = null)
private val gothicStyleOption =
- ListUserStyleSetting.ListOption("gothic_style", "Gothic", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("gothic_style"), "Gothic", icon = null)
private val watchHandStyleList =
listOf(classicStyleOption, modernStyleOption, gothicStyleOption)
private val watchHandStyleSetting = ListUserStyleSetting(
- "hand_style_setting",
+ UserStyleSetting.Id("hand_style_setting"),
"Hand Style",
"Hand visual look", /* icon = */
null,
watchHandStyleList,
- listOf(Layer.TOP_LAYER)
+ listOf(Layer.COMPLICATIONS_OVERLAY)
)
private val badStyleOption =
- ListUserStyleSetting.ListOption("bad_option", "Bad", icon = null)
+ ListUserStyleSetting.ListOption(Option.Id("bad_option"), "Bad", icon = null)
private val leftComplication =
Complication.createRoundRectComplicationBuilder(
@@ -217,7 +219,7 @@
.build()
private val leftAndRightComplicationsOption = ComplicationsOption(
- LEFT_AND_RIGHT_COMPLICATIONS,
+ Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Both",
null,
listOf(
@@ -228,7 +230,7 @@
)
)
private val noComplicationsOption = ComplicationsOption(
- NO_COMPLICATIONS,
+ Option.Id(NO_COMPLICATIONS),
"Both",
null,
listOf(
@@ -239,7 +241,7 @@
)
)
private val leftComplicationsOption = ComplicationsOption(
- LEFT_COMPLICATION,
+ Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(
@@ -250,7 +252,7 @@
)
)
private val rightComplicationsOption = ComplicationsOption(
- RIGHT_COMPLICATION,
+ Option.Id(RIGHT_COMPLICATION),
"Right",
null,
listOf(
@@ -261,7 +263,7 @@
)
)
private val complicationsStyleSetting = ComplicationsUserStyleSetting(
- "complications_style_setting",
+ UserStyleSetting.Id("complications_style_setting"),
"Complications",
"Number and position",
icon = null,
@@ -276,11 +278,11 @@
private lateinit var renderer: TestRenderer
private lateinit var complicationsManager: ComplicationsManager
- private lateinit var userStyleRepository: UserStyleRepository
+ private lateinit var currentUserStyleRepository: CurrentUserStyleRepository
private lateinit var watchFaceImpl: WatchFaceImpl
private lateinit var testWatchFaceService: TestWatchFaceService
private lateinit var engineWrapper: WatchFaceService.EngineWrapper
- private lateinit var interactiveWatchFaceInstanceWCS: IInteractiveWatchFaceWCS
+ private lateinit var interactiveWatchFaceInstance: IInteractiveWatchFace
private class Task(val runTimeMillis: Long, val runnable: Runnable) : Comparable<Task> {
override fun compareTo(other: Task) = runTimeMillis.compareTo(other.runTimeMillis)
@@ -320,11 +322,11 @@
hasBurnInProtection: Boolean = false,
tapListener: WatchFace.TapListener? = null
) {
- userStyleRepository = UserStyleRepository(userStyleSchema)
- complicationsManager = ComplicationsManager(complications, userStyleRepository)
+ currentUserStyleRepository = CurrentUserStyleRepository(userStyleSchema)
+ complicationsManager = ComplicationsManager(complications, currentUserStyleRepository)
renderer = TestRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState.asWatchState(),
INTERACTIVE_UPDATE_RATE_MS
)
@@ -332,7 +334,7 @@
watchFaceType,
complicationsManager,
renderer,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
handler,
tapListener,
@@ -358,11 +360,11 @@
userStyleSchema: UserStyleSchema,
wallpaperInteractiveWatchFaceInstanceParams: WallpaperInteractiveWatchFaceInstanceParams
) {
- userStyleRepository = UserStyleRepository(userStyleSchema)
- complicationsManager = ComplicationsManager(complications, userStyleRepository)
+ currentUserStyleRepository = CurrentUserStyleRepository(userStyleSchema)
+ complicationsManager = ComplicationsManager(complications, currentUserStyleRepository)
renderer = TestRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState.asWatchState(),
INTERACTIVE_UPDATE_RATE_MS
)
@@ -370,7 +372,7 @@
watchFaceType,
complicationsManager,
renderer,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
handler,
null,
@@ -382,14 +384,14 @@
.getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
wallpaperInteractiveWatchFaceInstanceParams,
- object : IPendingInteractiveWatchFaceWCS.Stub() {
+ object : IPendingInteractiveWatchFace.Stub() {
override fun getApiVersion() =
- IPendingInteractiveWatchFaceWCS.API_VERSION
+ IPendingInteractiveWatchFace.API_VERSION
- override fun onInteractiveWatchFaceWcsCreated(
- iInteractiveWatchFaceWcs: IInteractiveWatchFaceWCS
+ override fun onInteractiveWatchFaceCreated(
+ iInteractiveWatchFace: IInteractiveWatchFace
) {
- interactiveWatchFaceInstanceWCS = iInteractiveWatchFaceWcs
+ interactiveWatchFaceInstance = iInteractiveWatchFace
}
}
)
@@ -499,8 +501,8 @@
@After
public fun validate() {
- if (this::interactiveWatchFaceInstanceWCS.isInitialized) {
- interactiveWatchFaceInstanceWCS.release()
+ if (this::interactiveWatchFaceInstance.isInitialized) {
+ interactiveWatchFaceInstance.release()
}
if (this::engineWrapper.isInitialized && !engineWrapper.destroyed) {
@@ -639,8 +641,8 @@
private fun tapAt(x: Int, y: Int) {
// The eventTime is ignored.
- watchFaceImpl.onTapCommand(TapType.TOUCH, x, y)
- watchFaceImpl.onTapCommand(TapType.TAP, x, y)
+ watchFaceImpl.onTapCommand(TapType.DOWN, x, y)
+ watchFaceImpl.onTapCommand(TapType.UP, x, y)
}
private fun doubleTapAt(x: Int, y: Int, delayMillis: Long) {
@@ -658,8 +660,8 @@
}
private fun tapCancelAt(x: Int, y: Int) {
- watchFaceImpl.onTapCommand(TapType.TOUCH, x, y)
- watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, x, y)
+ watchFaceImpl.onTapCommand(TapType.DOWN, x, y)
+ watchFaceImpl.onTapCommand(TapType.CANCEL, x, y)
}
@Test
@@ -792,13 +794,13 @@
testWatchFaceService.reset()
// Tap down left Complication
- watchFaceImpl.onTapCommand(TapType.TOUCH, 30, 50)
+ watchFaceImpl.onTapCommand(TapType.DOWN, 30, 50)
// Tap down at right complication
- watchFaceImpl.onTapCommand(TapType.TOUCH, 70, 50)
+ watchFaceImpl.onTapCommand(TapType.DOWN, 70, 50)
// Now Tap cancel at the second position
- watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, 70, 50)
+ watchFaceImpl.onTapCommand(TapType.CANCEL, 70, 50)
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
assertThat(testWatchFaceService.complicationSingleTapped).isEqualTo(RIGHT_COMPLICATION_ID)
assertThat(testWatchFaceService.singleTapCount).isEqualTo(1)
@@ -814,9 +816,9 @@
testWatchFaceService.reset()
// Tap down at a position in left Complication
- watchFaceImpl.onTapCommand(TapType.TOUCH, 30, 50)
+ watchFaceImpl.onTapCommand(TapType.DOWN, 30, 50)
// Tap cancel at different position stillin left Complication
- watchFaceImpl.onTapCommand(TapType.TOUCH_CANCEL, 32, 50)
+ watchFaceImpl.onTapCommand(TapType.CANCEL, 32, 50)
runPostedTasksFor(ViewConfiguration.getDoubleTapTimeout().toLong())
assertThat(testWatchFaceService.complicationSingleTapped).isNull()
@@ -860,8 +862,8 @@
// Tap on nothing.
tapAt(1, 1)
- verify(tapListener).onTap(TapType.TOUCH, 1, 1)
- verify(tapListener).onTap(TapType.TAP, 1, 1)
+ verify(tapListener).onTap(TapType.DOWN, 1, 1)
+ verify(tapListener).onTap(TapType.UP, 1, 1)
}
@Test
@@ -876,8 +878,8 @@
// Tap right complication.
tapAt(70, 50)
- verify(tapListener, times(0)).onTap(TapType.TOUCH, 70, 50)
- verify(tapListener, times(0)).onTap(TapType.TAP, 70, 50)
+ verify(tapListener, times(0)).onTap(TapType.DOWN, 70, 50)
+ verify(tapListener, times(0)).onTap(TapType.UP, 70, 50)
}
@Test
@@ -1062,7 +1064,7 @@
)
// This should get persisted.
- userStyleRepository.userStyle = UserStyle(
+ currentUserStyleRepository.userStyle = UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
watchHandStyleSetting to gothicStyleOption
@@ -1070,7 +1072,7 @@
)
engineWrapper.onDestroy()
- val userStyleRepository2 = UserStyleRepository(
+ val userStyleRepository2 = CurrentUserStyleRepository(
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting))
)
@@ -1125,7 +1127,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
@@ -1137,11 +1139,11 @@
)
// The style option above should get applied during watch face creation.
- assertThat(userStyleRepository.userStyle.selectedOptions[colorStyleSetting]!!.id)
+ assertThat(currentUserStyleRepository.userStyle.selectedOptions[colorStyleSetting]!!.id)
.isEqualTo(
blueStyleOption.id
)
- assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting]!!.id)
+ assertThat(currentUserStyleRepository.userStyle.selectedOptions[watchHandStyleSetting]!!.id)
.isEqualTo(
gothicStyleOption.id
)
@@ -1161,13 +1163,13 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(hashMapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null
)
)
- assertThat(userStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
+ assertThat(currentUserStyleRepository.userStyle.selectedOptions[watchHandStyleSetting])
.isEqualTo(watchHandStyleList.first())
}
@@ -1215,7 +1217,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(hashMapOf(watchHandStyleSetting to badStyleOption)).toWireFormat(),
null
)
@@ -1367,18 +1369,19 @@
@Test
public fun getOptionForIdentifier_ListViewStyleSetting() {
// Check the correct Options are returned for known option names.
- assertThat(colorStyleSetting.getOptionForId(redStyleOption.id)).isEqualTo(
+ assertThat(colorStyleSetting.getOptionForId(redStyleOption.id.value)).isEqualTo(
redStyleOption
)
- assertThat(colorStyleSetting.getOptionForId(greenStyleOption.id)).isEqualTo(
+ assertThat(colorStyleSetting.getOptionForId(greenStyleOption.id.value)).isEqualTo(
greenStyleOption
)
- assertThat(colorStyleSetting.getOptionForId(blueStyleOption.id)).isEqualTo(
+ assertThat(colorStyleSetting.getOptionForId(blueStyleOption.id.value)).isEqualTo(
blueStyleOption
)
// For unknown option names the first element in the list should be returned.
- assertThat(colorStyleSetting.getOptionForId("unknown")).isEqualTo(colorStyleList.first())
+ assertThat(colorStyleSetting.getOptionForId("unknown".encodeToByteArray()))
+ .isEqualTo(colorStyleList.first())
}
@Test
@@ -1406,7 +1409,7 @@
@Test
public fun requestStyleBeforeSetBinder() {
var userStyleRepository =
- UserStyleRepository(UserStyleSchema(emptyList()))
+ CurrentUserStyleRepository(UserStyleSchema(emptyList()))
var testRenderer = TestRenderer(
surfaceHolder,
userStyleRepository,
@@ -1420,7 +1423,7 @@
userStyleRepository
),
testRenderer,
- UserStyleRepository(UserStyleSchema(emptyList())),
+ CurrentUserStyleRepository(UserStyleSchema(emptyList())),
watchState,
handler,
null,
@@ -1518,7 +1521,7 @@
1000,
2000,
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
@@ -1548,7 +1551,7 @@
1000,
2000,
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
@@ -1626,7 +1629,7 @@
@Test
public fun shouldAnimateOverrideControlsEnteringAmbientMode() {
- var userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+ var userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
var testRenderer = object : TestRenderer(
surfaceHolder,
userStyleRepository,
@@ -1640,7 +1643,7 @@
WatchFaceType.ANALOG,
ComplicationsManager(emptyList(), userStyleRepository),
testRenderer,
- UserStyleRepository(UserStyleSchema(emptyList())),
+ CurrentUserStyleRepository(UserStyleSchema(emptyList())),
watchState,
handler,
null,
@@ -1679,9 +1682,9 @@
)
// Select a new style which turns off both complications.
- val newStyleA = HashMap(userStyleRepository.userStyle.selectedOptions)
+ val newStyleA = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
newStyleA[complicationsStyleSetting] = noComplicationsOption
- userStyleRepository.userStyle = UserStyle(newStyleA)
+ currentUserStyleRepository.userStyle = UserStyle(newStyleA)
runPostedTasksFor(0)
@@ -1697,9 +1700,9 @@
reset(iWatchFaceService)
// Select a new style which turns on only the left complication.
- val newStyleB = HashMap(userStyleRepository.userStyle.selectedOptions)
+ val newStyleB = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
newStyleB[complicationsStyleSetting] = leftComplicationsOption
- userStyleRepository.userStyle = UserStyle(newStyleB)
+ currentUserStyleRepository.userStyle = UserStyle(newStyleB)
runPostedTasksFor(0)
@@ -1717,26 +1720,26 @@
@Test
public fun partialComplicationOverrides() {
val bothComplicationsOption = ComplicationsOption(
- LEFT_AND_RIGHT_COMPLICATIONS,
+ Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left And Right",
null,
// An empty list means use the initial config.
emptyList()
)
val leftOnlyComplicationsOption = ComplicationsOption(
- LEFT_COMPLICATION,
+ Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(ComplicationOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
)
val rightOnlyComplicationsOption = ComplicationsOption(
- RIGHT_COMPLICATION,
+ Option.Id(RIGHT_COMPLICATION),
"Right",
null,
listOf(ComplicationOverlay.Builder(LEFT_COMPLICATION_ID).setEnabled(false).build())
)
val complicationsStyleSetting = ComplicationsUserStyleSetting(
- "complications_style_setting",
+ UserStyleSetting.Id("complications_style_setting"),
"Complications",
"Number and position",
icon = null,
@@ -1759,9 +1762,9 @@
assertTrue(rightComplication.enabled)
// Select left complication only.
- val newStyleA = HashMap(userStyleRepository.userStyle.selectedOptions)
+ val newStyleA = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
newStyleA[complicationsStyleSetting] = leftOnlyComplicationsOption
- userStyleRepository.userStyle = UserStyle(newStyleA)
+ currentUserStyleRepository.userStyle = UserStyle(newStyleA)
runPostedTasksFor(0)
@@ -1769,9 +1772,9 @@
assertFalse(rightComplication.enabled)
// Select right complication only.
- val newStyleB = HashMap(userStyleRepository.userStyle.selectedOptions)
+ val newStyleB = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
newStyleB[complicationsStyleSetting] = rightOnlyComplicationsOption
- userStyleRepository.userStyle = UserStyle(newStyleB)
+ currentUserStyleRepository.userStyle = UserStyle(newStyleB)
runPostedTasksFor(0)
@@ -1779,9 +1782,9 @@
assertTrue(rightComplication.enabled)
// Select both complications.
- val newStyleC = HashMap(userStyleRepository.userStyle.selectedOptions)
+ val newStyleC = HashMap(currentUserStyleRepository.userStyle.selectedOptions)
newStyleC[complicationsStyleSetting] = bothComplicationsOption
- userStyleRepository.userStyle = UserStyle(newStyleC)
+ currentUserStyleRepository.userStyle = UserStyle(newStyleC)
runPostedTasksFor(0)
@@ -1792,20 +1795,20 @@
@Test
public fun partialComplicationOverrideAppliedToInitialStyle() {
val bothComplicationsOption = ComplicationsOption(
- LEFT_AND_RIGHT_COMPLICATIONS,
+ Option.Id(LEFT_AND_RIGHT_COMPLICATIONS),
"Left And Right",
null,
// An empty list means use the initial config.
emptyList()
)
val leftOnlyComplicationsOption = ComplicationsOption(
- LEFT_COMPLICATION,
+ Option.Id(LEFT_COMPLICATION),
"Left",
null,
listOf(ComplicationOverlay.Builder(RIGHT_COMPLICATION_ID).setEnabled(false).build())
)
val complicationsStyleSetting = ComplicationsUserStyleSetting(
- "complications_style_setting",
+ UserStyleSetting.Id("complications_style_setting"),
"Complications",
"Number and position",
icon = null,
@@ -1841,7 +1844,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
@@ -1858,7 +1861,7 @@
rightComplicationData = it.asWireComplicationData()
}
- interactiveWatchFaceInstanceWCS.updateComplicationData(
+ interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
@@ -1895,13 +1898,13 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
- interactiveWatchFaceInstanceWCS.updateComplicationData(
+ interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
@@ -1916,7 +1919,7 @@
assertThat(leftComplication.isActiveAt(0)).isTrue()
// Send empty data.
- interactiveWatchFaceInstanceWCS.updateComplicationData(
+ interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
@@ -1928,7 +1931,7 @@
assertThat(leftComplication.isActiveAt(0)).isFalse()
// Send a complication that is active for a time range.
- interactiveWatchFaceInstanceWCS.updateComplicationData(
+ interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
LEFT_COMPLICATION_ID,
@@ -1961,14 +1964,14 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
)
// Send a complication with an invalid id - this should get ignored.
- interactiveWatchFaceInstanceWCS.updateComplicationData(
+ interactiveWatchFaceInstance.updateComplicationData(
listOf(
IdAndComplicationDataWireFormat(
RIGHT_COMPLICATION_ID,
@@ -1984,7 +1987,7 @@
public fun invalidateRendererBeforeFullInit() {
renderer = TestRenderer(
surfaceHolder,
- UserStyleRepository(UserStyleSchema(emptyList())),
+ CurrentUserStyleRepository(UserStyleSchema(emptyList())),
watchState.asWatchState(),
INTERACTIVE_UPDATE_RATE_MS
)
@@ -2007,7 +2010,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
@@ -2033,7 +2036,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
null
)
@@ -2075,11 +2078,11 @@
@Test
public fun isAmbientInitalisedEvenWithoutPropertiesSent() {
- userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
- complicationsManager = ComplicationsManager(emptyList(), userStyleRepository)
+ currentUserStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
+ complicationsManager = ComplicationsManager(emptyList(), currentUserStyleRepository)
renderer = TestRenderer(
surfaceHolder,
- userStyleRepository,
+ currentUserStyleRepository,
watchState.asWatchState(),
INTERACTIVE_UPDATE_RATE_MS
)
@@ -2087,7 +2090,7 @@
WatchFaceType.ANALOG,
complicationsManager,
renderer,
- userStyleRepository,
+ currentUserStyleRepository,
watchState,
handler,
tapListener,
@@ -2120,7 +2123,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(hashMapOf(colorStyleSetting to blueStyleOption)).toWireFormat(),
null
)
@@ -2135,9 +2138,9 @@
val params = RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
null
)
@@ -2153,9 +2156,9 @@
RenderParameters(
DrawMode.INTERACTIVE,
mapOf(
- Layer.BASE_LAYER to LayerMode.DRAW,
+ Layer.BASE to LayerMode.DRAW,
Layer.COMPLICATIONS to LayerMode.DRAW_OUTLINED,
- Layer.TOP_LAYER to LayerMode.DRAW
+ Layer.COMPLICATIONS_OVERLAY to LayerMode.DRAW
),
null
)
@@ -2207,7 +2210,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
listOf(
IdAndComplicationDataWireFormat(
@@ -2241,7 +2244,7 @@
@Test
public fun directBoot() {
- val userStyleRepository = UserStyleRepository(
+ val userStyleRepository = CurrentUserStyleRepository(
UserStyleSchema(listOf(colorStyleSetting, watchHandStyleSetting))
)
val testRenderer = TestRenderer(
@@ -2268,7 +2271,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
@@ -2294,7 +2297,7 @@
@Test
public fun headlessFlagPreventsDirectBoot() {
- val userStyleRepository = UserStyleRepository(UserStyleSchema(emptyList()))
+ val userStyleRepository = CurrentUserStyleRepository(UserStyleSchema(emptyList()))
val testRenderer = TestRenderer(
surfaceHolder,
userStyleRepository,
@@ -2319,7 +2322,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(
hashMapOf(
colorStyleSetting to blueStyleOption,
@@ -2352,7 +2355,7 @@
0,
0
),
- SystemState(false, 0),
+ WatchUiState(false, 0),
UserStyle(emptyMap()).toWireFormat(),
listOf(
IdAndComplicationDataWireFormat(
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
index 5d58caf..29f1844 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProxyOverrideActivity.java
@@ -113,7 +113,9 @@
@Override
protected void onDestroy() {
- mProxy.shutdown();
+ if (mProxy != null) {
+ mProxy.shutdown();
+ }
super.onDestroy();
}
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
similarity index 61%
rename from webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java
rename to webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
index 328af59..e882b7b 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptReferenceImpl.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/ScriptHandlerImpl.java
@@ -19,30 +19,30 @@
import androidx.annotation.NonNull;
import androidx.webkit.ScriptHandler;
-import org.chromium.support_lib_boundary.ScriptReferenceBoundaryInterface;
+import org.chromium.support_lib_boundary.ScriptHandlerBoundaryInterface;
import org.chromium.support_lib_boundary.util.BoundaryInterfaceReflectionUtil;
import java.lang.reflect.InvocationHandler;
/**
- * Internal implementation of {@link androidx.webkit.ScriptReference}.
+ * Internal implementation of {@link androidx.webkit.ScriptHandler}.
*/
-public class ScriptReferenceImpl extends ScriptHandler {
- private ScriptReferenceBoundaryInterface mBoundaryInterface;
+public class ScriptHandlerImpl extends ScriptHandler {
+ private ScriptHandlerBoundaryInterface mBoundaryInterface;
- private ScriptReferenceImpl(@NonNull ScriptReferenceBoundaryInterface boundaryInterface) {
+ private ScriptHandlerImpl(@NonNull ScriptHandlerBoundaryInterface boundaryInterface) {
mBoundaryInterface = boundaryInterface;
}
/**
- * Create an AndroidX ScriptReference from the given InvocationHandler.
+ * Create an AndroidX ScriptHandler from the given InvocationHandler.
*/
- public static @NonNull ScriptReferenceImpl toScriptHandler(
- @NonNull /* ScriptReference */ InvocationHandler invocationHandler) {
- final ScriptReferenceBoundaryInterface boundaryInterface =
+ public static @NonNull ScriptHandlerImpl toScriptHandler(
+ @NonNull /* ScriptHandler */ InvocationHandler invocationHandler) {
+ final ScriptHandlerBoundaryInterface boundaryInterface =
BoundaryInterfaceReflectionUtil.castToSuppLibClass(
- ScriptReferenceBoundaryInterface.class, invocationHandler);
- return new ScriptReferenceImpl(boundaryInterface);
+ ScriptHandlerBoundaryInterface.class, invocationHandler);
+ return new ScriptHandlerImpl(boundaryInterface);
}
/**
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 69a2c41..919c549 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -102,9 +102,9 @@
* Adapter method for {@link WebViewCompat#addWebMessageListener(android.webkit.WebView,
* String, Set)}
*/
- public @NonNull ScriptReferenceImpl addDocumentStartJavaScript(
+ public @NonNull ScriptHandlerImpl addDocumentStartJavaScript(
@NonNull String script, @NonNull String[] allowedOriginRules) {
- return ScriptReferenceImpl.toScriptHandler(
+ return ScriptHandlerImpl.toScriptHandler(
mImpl.addDocumentStartJavaScript(script, allowedOriginRules));
}
diff --git a/window/window-extensions/api/current.txt b/window/window-extensions/api/current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/current.txt
+++ b/window/window-extensions/api/current.txt
@@ -21,7 +21,6 @@
method public int getState();
method public int getType();
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/api/public_plus_experimental_current.txt b/window/window-extensions/api/public_plus_experimental_current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/public_plus_experimental_current.txt
+++ b/window/window-extensions/api/public_plus_experimental_current.txt
@@ -21,7 +21,6 @@
method public int getState();
method public int getType();
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/api/restricted_current.txt b/window/window-extensions/api/restricted_current.txt
index 78bdc7f..486715e 100644
--- a/window/window-extensions/api/restricted_current.txt
+++ b/window/window-extensions/api/restricted_current.txt
@@ -21,7 +21,6 @@
method public int getState();
method public int getType();
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
diff --git a/window/window-extensions/build.gradle b/window/window-extensions/build.gradle
index 5b1a650..384720b 100644
--- a/window/window-extensions/build.gradle
+++ b/window/window-extensions/build.gradle
@@ -31,12 +31,6 @@
id("com.android.library")
}
-android {
- defaultConfig {
- minSdkVersion 14
- }
-}
-
dependencies {
implementation("androidx.annotation:annotation:1.1.0")
diff --git a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
index fdd42fa..5c46471 100644
--- a/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
+++ b/window/window-extensions/src/androidTest/java/androidx/window/extensions/ExtensionDisplayFeatureTest.java
@@ -77,7 +77,7 @@
Rect rect = new Rect(1, 0, 1, 10);
int type = ExtensionFoldingFeature.TYPE_FOLD;
int originalState = ExtensionFoldingFeature.STATE_FLAT;
- int otherState = ExtensionFoldingFeature.STATE_FLIPPED;
+ int otherState = ExtensionFoldingFeature.STATE_HALF_OPENED;
ExtensionFoldingFeature original = new ExtensionFoldingFeature(rect, type,
originalState);
diff --git a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
index bc7e5b5..05232380 100644
--- a/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
+++ b/window/window-extensions/src/main/java/androidx/window/extensions/ExtensionFoldingFeature.java
@@ -65,19 +65,10 @@
*/
public static final int STATE_HALF_OPENED = 2;
- /**
- * The foldable device's hinge is flipped with the flexible screen parts or physical screens
- * facing opposite directions. See the
- * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
- * section in the official documentation for visual samples and references.
- */
- public static final int STATE_FLIPPED = 3;
-
@Retention(RetentionPolicy.SOURCE)
@IntDef({
STATE_HALF_OPENED,
- STATE_FLAT,
- STATE_FLIPPED
+ STATE_FLAT
})
@interface State {}
@@ -156,8 +147,6 @@
switch (state) {
case STATE_FLAT:
return "FLAT";
- case STATE_FLIPPED:
- return "FLIPPED";
case STATE_HALF_OPENED:
return "HALF_OPENED";
default:
diff --git a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
index b24539b..57ce8be 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/PresentationActivity.kt
@@ -162,7 +162,6 @@
private fun FoldingFeature.stateString(): String {
return when (state) {
FoldingFeature.STATE_FLAT -> "FLAT"
- FoldingFeature.STATE_FLIPPED -> "FLIPPED"
FoldingFeature.STATE_HALF_OPENED -> "HALF_OPENED"
else -> "Unknown feature state ($state)"
}
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
index b0d6db3..4a77e93 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/ActivityExtensions.kt
@@ -18,18 +18,13 @@
import android.app.Activity
import android.graphics.Point
-import android.util.DisplayMetrics
+import androidx.window.WindowManager
/**
* Return a [Point] whose dimensions match the metrics of the window.
* @return [Point] whose dimensions match the metrics of the window.
*/
-@Suppress("DEPRECATION")
internal fun Activity.calculateWindowSizeExt(): Point {
- val displayMetrics = DisplayMetrics()
- // TODO(b/159454816) Replace with window metrics.
- this.windowManager.defaultDisplay
- .getMetrics(displayMetrics)
-
- return Point(displayMetrics.widthPixels, displayMetrics.heightPixels)
+ val bounds = WindowManager(this).currentWindowMetrics.bounds
+ return Point(bounds.width(), bounds.height())
}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
index af7dbca..cf7b124 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/backend/MidScreenFoldBackend.kt
@@ -19,11 +19,9 @@
package androidx.window.sample.backend
import android.app.Activity
-import android.content.Context
import android.graphics.Point
import android.graphics.Rect
import androidx.core.util.Consumer
-import androidx.window.DeviceState
import androidx.window.DisplayFeature
import androidx.window.FoldingFeature
import androidx.window.WindowBackend
@@ -76,14 +74,6 @@
return WindowLayoutInfo.Builder().setDisplayFeatures(featureList).build()
}
- @Deprecated("Added for compatibility with WindowBackend in sample")
- override fun registerLayoutChangeCallback(
- context: Context,
- executor: Executor,
- callback: Consumer<WindowLayoutInfo>
- ) {
- }
-
private fun foldRect(windowSize: Point): Rect {
return when (foldAxis) {
FoldAxis.LONG_DIMENSION -> longDimensionFold(windowSize)
@@ -107,15 +97,6 @@
}
}
- override fun registerDeviceStateChangeCallback(
- executor: Executor,
- callback: Consumer<DeviceState>
- ) {
- }
-
- override fun unregisterDeviceStateChangeCallback(callback: Consumer<DeviceState>) {
- }
-
override fun registerLayoutChangeCallback(
activity: Activity,
executor: Executor,
diff --git a/window/window-sidecar/build.gradle b/window/window-sidecar/build.gradle
index d8b8fd3..36be655 100644
--- a/window/window-sidecar/build.gradle
+++ b/window/window-sidecar/build.gradle
@@ -24,12 +24,6 @@
id("com.android.library")
}
-android {
- defaultConfig {
- minSdkVersion 14
- }
-}
-
dependencies {
implementation("androidx.annotation:annotation:1.1.0")
}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -43,17 +43,13 @@
field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
field public static final int ORIENTATION_VERTICAL = 0; // 0x0
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
public interface WindowBackend {
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -43,17 +43,13 @@
field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
field public static final int ORIENTATION_VERTICAL = 0; // 0x0
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
public interface WindowBackend {
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index e8f74ed..bc9a317 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -43,17 +43,13 @@
field public static final int ORIENTATION_HORIZONTAL = 1; // 0x1
field public static final int ORIENTATION_VERTICAL = 0; // 0x0
field public static final int STATE_FLAT = 1; // 0x1
- field public static final int STATE_FLIPPED = 3; // 0x3
field public static final int STATE_HALF_OPENED = 2; // 0x2
field public static final int TYPE_FOLD = 1; // 0x1
field public static final int TYPE_HINGE = 2; // 0x2
}
public interface WindowBackend {
- method public void registerDeviceStateChangeCallback(java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void registerLayoutChangeCallback(android.app.Activity, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method @Deprecated public void registerLayoutChangeCallback(android.content.Context, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
- method public void unregisterDeviceStateChangeCallback(androidx.core.util.Consumer<androidx.window.DeviceState!>);
method public void unregisterLayoutChangeCallback(androidx.core.util.Consumer<androidx.window.WindowLayoutInfo!>);
}
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 706f6f6..4a24e84 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -35,9 +35,6 @@
}
android {
- defaultConfig {
- minSdkVersion 14
- }
buildTypes.all {
consumerProguardFiles "proguard-rules.pro"
}
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
index e78d762..0d41ab5 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionCompatTest.java
@@ -141,7 +141,7 @@
Rect bounds = new Rect(WINDOW_BOUNDS.left, WINDOW_BOUNDS.top, WINDOW_BOUNDS.width(), 1);
ExtensionDisplayFeature extensionDisplayFeature =
new ExtensionFoldingFeature(bounds, ExtensionFoldingFeature.TYPE_HINGE,
- ExtensionFoldingFeature.STATE_FLIPPED);
+ ExtensionFoldingFeature.STATE_HALF_OPENED);
List<ExtensionDisplayFeature> displayFeatures = new ArrayList<>();
displayFeatures.add(extensionDisplayFeature);
ExtensionWindowLayoutInfo extensionWindowLayoutInfo =
diff --git a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
index 7a55cb9..2648fb6 100644
--- a/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/ExtensionWindowBackendTest.java
@@ -16,19 +16,12 @@
package androidx.window;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -42,8 +35,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
-import com.google.common.collect.BoundType;
-import com.google.common.collect.Range;
import com.google.common.util.concurrent.MoreExecutors;
import org.junit.Before;
@@ -104,33 +95,6 @@
}
@Test
- public void testRegisterDeviceStateChangeCallback_noExtension() {
- // Verify method with extension
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- assumeTrue(backend.mWindowExtension == null);
- SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
-
- backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
-
- DeviceState deviceState = simpleConsumer.lastValue();
- assertNotNull(deviceState);
- assertThat(deviceState.getPosture()).isIn(Range.range(
- DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
- DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
- DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
-
- // Verify method without extension
- backend.mWindowExtension = null;
- SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
- backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
- deviceState = noExtensionConsumer.lastValue();
- assertNotNull(deviceState);
- assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
- // Verify that last reported state does not change when using the getter
- assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
- }
-
- @Test
public void testRegisterLayoutChangeCallback() {
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
@@ -232,117 +196,6 @@
assertEquals(expected, consumer.mValues);
}
- @Test
- public void testRegisterDeviceChangeCallback() {
- ExtensionInterfaceCompat mockInterface = mock(
- ExtensionInterfaceCompat.class);
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mockInterface;
-
- // Check registering the device state change callback
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
- assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
-
- // Check unregistering the device state change callback
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
- }
-
- @Test
- public void testDeviceChangeCallback() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check that callbacks from the extension are propagated correctly
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- DeviceState deviceState = newTestDeviceState();
- ExtensionWindowBackend.ExtensionListenerImpl backendListener =
- backend.new ExtensionListenerImpl();
- backendListener.onDeviceStateChanged(deviceState);
-
- verify(consumer).accept(eq(deviceState));
- assertEquals(deviceState, backend.mLastReportedDeviceState);
-
- // Test that the same value wouldn't be reported again
- reset(consumer);
- backendListener.onDeviceStateChanged(deviceState);
- verify(consumer, never()).accept(any());
- }
-
- @Test
- public void testDeviceChangeChangeCallback_callsExtensionOnce() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check registering the layout change callback
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- mActivityTestRule.launchActivity(new Intent());
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
-
- assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
-
- // Check unregistering the layout change callback
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension, times(0))
- .onDeviceStateListenersChanged(true);
- }
-
- @Test
- public void testDeviceChangeChangeCallback_clearListeners() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check registering the layout change callback
- Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
- Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
- mActivityTestRule.launchActivity(new Intent());
- backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
- backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
-
- // Check unregistering the layout change callback
- backend.unregisterDeviceStateChangeCallback(firstConsumer);
- backend.unregisterDeviceStateChangeCallback(secondConsumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
- }
-
- @Test
- public void testDeviceChangeCallback_relayLastEmittedValue() {
- DeviceState expectedState = newTestDeviceState();
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
- backend.mLastReportedDeviceState = expectedState;
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
- verify(consumer).accept(expectedState);
- }
-
- @Test
- public void testDeviceChangeCallback_clearLastEmittedValue() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- assertNull(backend.mLastReportedDeviceState);
- }
-
private static WindowLayoutInfo newTestWindowLayoutInfo() {
WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
WindowLayoutInfo windowLayoutInfo = builder.build();
diff --git a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
index fcf70d6..ffe4c7f 100644
--- a/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
+++ b/window/window/src/androidTest/java/androidx/window/FoldingFeatureTest.java
@@ -23,7 +23,6 @@
import static androidx.window.FoldingFeature.OcclusionType;
import static androidx.window.FoldingFeature.Orientation;
import static androidx.window.FoldingFeature.STATE_FLAT;
-import static androidx.window.FoldingFeature.STATE_FLIPPED;
import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
import static androidx.window.FoldingFeature.TYPE_FOLD;
import static androidx.window.FoldingFeature.TYPE_HINGE;
@@ -61,22 +60,22 @@
@Test(expected = IllegalArgumentException.class)
public void testHorizontalHingeWithNonZeroOrigin() {
- new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_FLIPPED);
+ new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_HINGE, STATE_HALF_OPENED);
}
@Test(expected = IllegalArgumentException.class)
public void testVerticalHingeWithNonZeroOrigin() {
- new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_FLIPPED);
+ new FoldingFeature(new Rect(10, 1, 19, 29), TYPE_HINGE, STATE_HALF_OPENED);
}
@Test(expected = IllegalArgumentException.class)
public void testHorizontalFoldWithNonZeroOrigin() {
- new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_FLIPPED);
+ new FoldingFeature(new Rect(1, 10, 20, 10), TYPE_FOLD, STATE_HALF_OPENED);
}
@Test(expected = IllegalArgumentException.class)
public void testVerticalFoldWithNonZeroOrigin() {
- new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_FLIPPED);
+ new FoldingFeature(new Rect(10, 1, 10, 20), TYPE_FOLD, STATE_HALF_OPENED);
}
@Test(expected = IllegalArgumentException.class)
@@ -145,7 +144,7 @@
Rect rect = new Rect(1, 0, 1, 10);
int type = TYPE_FOLD;
int originalState = STATE_FLAT;
- int otherState = STATE_FLIPPED;
+ int otherState = STATE_HALF_OPENED;
FoldingFeature original = new FoldingFeature(rect, type, originalState);
FoldingFeature other = new FoldingFeature(rect, type, otherState);
diff --git a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
index 99e5f35..1ce4467 100644
--- a/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
+++ b/window/window/src/androidTest/java/androidx/window/SidecarCompatTest.java
@@ -431,16 +431,10 @@
.get(0);
assertEquals(FoldingFeature.STATE_HALF_OPENED, capturedFoldingFeature.getState());
- reset(mockCallback);
- fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
- verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
- capturedFoldingFeature = (FoldingFeature) windowLayoutCaptor.getValue().getDisplayFeatures()
- .get(0);
- assertEquals(FoldingFeature.STATE_FLIPPED, capturedFoldingFeature.getState());
-
- // No display features must be reported in closed state
+ // No display features must be reported in closed state or flipped state.
reset(mockCallback);
fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_CLOSED));
+ fakeSidecarImp.triggerDeviceState(newDeviceState(SidecarDeviceState.POSTURE_FLIPPED));
verify(mockCallback).onWindowLayoutChanged(eq(mActivity), windowLayoutCaptor.capture());
assertTrue(windowLayoutCaptor.getValue().getDisplayFeatures().isEmpty());
}
diff --git a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
index 4730074..34bbc2a 100644
--- a/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
+++ b/window/window/src/androidTest/java/androidx/window/WindowBackendTest.java
@@ -20,7 +20,6 @@
import static org.mockito.Mockito.verify;
import android.app.Activity;
-import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
@@ -77,20 +76,6 @@
mWindowLayoutInfo = windowLayoutInfo;
}
- /**
- * Throws an exception if used.
- * @deprecated will be removed in next alpha
- * @param context any {@link Activity}
- * @param executor any {@link Executor}
- * @param callback any {@link Consumer}
- */
- @Override
- @Deprecated // TODO(b/173739071) Remove in next alpha.
- public void registerLayoutChangeCallback(@NonNull Context context,
- @NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
- throw new RuntimeException("Deprecated method");
- }
-
@Override
public void registerLayoutChangeCallback(@NonNull Activity activity,
@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
@@ -102,15 +87,5 @@
// Empty
}
- @Override
- public void registerDeviceStateChangeCallback(@NonNull Executor executor,
- @NonNull Consumer<DeviceState> callback) {
- throw new UnsupportedOperationException("Deprecated method");
- }
-
- @Override
- public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
- throw new UnsupportedOperationException("Deprecated method");
- }
}
}
diff --git a/window/window/src/main/java/androidx/window/ExtensionAdapter.java b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
index 4285b53..2a66c34 100644
--- a/window/window/src/main/java/androidx/window/ExtensionAdapter.java
+++ b/window/window/src/main/java/androidx/window/ExtensionAdapter.java
@@ -116,9 +116,6 @@
case ExtensionFoldingFeature.STATE_FLAT:
state = FoldingFeature.STATE_FLAT;
break;
- case ExtensionFoldingFeature.STATE_FLIPPED:
- state = FoldingFeature.STATE_FLIPPED;
- break;
case ExtensionFoldingFeature.STATE_HALF_OPENED:
state = FoldingFeature.STATE_HALF_OPENED;
break;
diff --git a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
index 692e3c7..0476c5d 100644
--- a/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
+++ b/window/window/src/main/java/androidx/window/ExtensionWindowBackend.java
@@ -21,7 +21,6 @@
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
-import android.content.ContextWrapper;
import android.util.Log;
import androidx.annotation.GuardedBy;
@@ -96,45 +95,6 @@
}
@Override
- public void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
- @NonNull Consumer<WindowLayoutInfo> callback) {
- registerLayoutChangeCallback(assertActivityContext(context), executor, callback);
- }
-
- /**
- * Unwraps the hierarchy of {@link ContextWrapper}-s until {@link Activity} is reached.
- * @return Base {@link Activity} context or {@code null} if not available.
- * @deprecated added temporarily to make migration easier. Will be removed in next relesae.
- */
- @Nullable
- @Deprecated // TODO(b/173739071) remove
- private static Activity getActivityFromContext(Context context) {
- while (context instanceof ContextWrapper) {
- if (context instanceof Activity) {
- return (Activity) context;
- }
- context = ((ContextWrapper) context).getBaseContext();
- }
- return null;
- }
-
- /**
- * @deprecated added temporarily to make migration easier. Will be removed in next release.
- * @param context any {@link Context}
- * @return {@link Activity} if associated with {@link Context} throw
- * {@link IllegalArgumentException} otherwise.
- */
- @Deprecated
- private Activity assertActivityContext(Context context) {
- Activity activity = getActivityFromContext(context);
- if (activity == null) {
- throw new IllegalArgumentException("Used non-visual Context with WindowManager. "
- + "Please use an Activity or a ContextWrapper around an Activity instead.");
- }
- return activity;
- }
-
- @Override
public void registerLayoutChangeCallback(@NonNull Activity activity,
@NonNull Executor executor, @NonNull Consumer<WindowLayoutInfo> callback) {
synchronized (sLock) {
@@ -211,55 +171,6 @@
mWindowExtension.onWindowLayoutChangeListenerRemoved(activity);
}
- @Override
- public void registerDeviceStateChangeCallback(@NonNull Executor executor,
- @NonNull Consumer<DeviceState> callback) {
- synchronized (sLock) {
- final DeviceStateChangeCallbackWrapper callbackWrapper =
- new DeviceStateChangeCallbackWrapper(executor, callback);
- if (mWindowExtension == null) {
- if (DEBUG) {
- Log.d(TAG, "Extension not loaded, skipping callback registration.");
- }
- callback.accept(new DeviceState(DeviceState.POSTURE_UNKNOWN));
- return;
- }
-
- if (mDeviceStateChangeCallbacks.isEmpty()) {
- mWindowExtension.onDeviceStateListenersChanged(false /* isEmpty */);
- }
-
- mDeviceStateChangeCallbacks.add(callbackWrapper);
- if (mLastReportedDeviceState != null) {
- callbackWrapper.accept(mLastReportedDeviceState);
- }
- }
- }
-
- @Override
- public void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback) {
- synchronized (sLock) {
- if (mWindowExtension == null) {
- if (DEBUG) {
- Log.d(TAG, "Extension not loaded, skipping callback un-registration.");
- }
- return;
- }
-
- for (DeviceStateChangeCallbackWrapper callbackWrapper : mDeviceStateChangeCallbacks) {
- if (callbackWrapper.mCallback.equals(callback)) {
- mDeviceStateChangeCallbacks.remove(callbackWrapper);
- if (mDeviceStateChangeCallbacks.isEmpty()) {
- mWindowExtension.onDeviceStateListenersChanged(true /* isEmpty */);
- // Clear device state so we do not replay stale data.
- mLastReportedDeviceState = null;
- }
- return;
- }
- }
- }
- }
-
@VisibleForTesting
class ExtensionListenerImpl implements ExtensionInterfaceCompat.ExtensionCallbackInterface {
@Override
diff --git a/window/window/src/main/java/androidx/window/FoldingFeature.java b/window/window/src/main/java/androidx/window/FoldingFeature.java
index 388cff7..cee6744 100644
--- a/window/window/src/main/java/androidx/window/FoldingFeature.java
+++ b/window/window/src/main/java/androidx/window/FoldingFeature.java
@@ -65,14 +65,6 @@
public static final int STATE_HALF_OPENED = 2;
/**
- * The foldable device is flipped with the flexible screen parts or physical screens facing
- * opposite directions. See the
- * <a href="https://developer.android.com/guide/topics/ui/foldables#postures">Posture</a>
- * section in the official documentation for visual samples and references.
- */
- public static final int STATE_FLIPPED = 3;
-
- /**
* The {@link FoldingFeature} does not occlude the content in any way. One example is a flat
* continuous fold where content can stretch across the fold. Another example is a hinge that
* has width or height equal to 0. In this case the content is physically split across both
@@ -116,7 +108,6 @@
@IntDef({
STATE_HALF_OPENED,
STATE_FLAT,
- STATE_FLIPPED,
})
@interface State {}
@@ -193,7 +184,7 @@
if (mType == TYPE_HINGE) {
return true;
}
- if (mType == TYPE_FOLD && (mState == STATE_FLIPPED || mState == STATE_HALF_OPENED)) {
+ if (mType == TYPE_FOLD && mState == STATE_HALF_OPENED) {
return true;
}
return false;
@@ -260,14 +251,13 @@
}
/**
- * Verifies the state is {@link FoldingFeature#STATE_FLAT},
- * {@link FoldingFeature#STATE_HALF_OPENED} or {@link FoldingFeature#STATE_FLIPPED}.
+ * Verifies the state is {@link FoldingFeature#STATE_FLAT} or
+ * {@link FoldingFeature#STATE_HALF_OPENED}.
*/
private static void validateState(int state) {
- if (state != STATE_FLAT && state != STATE_HALF_OPENED && state != STATE_FLIPPED) {
+ if (state != STATE_FLAT && state != STATE_HALF_OPENED) {
throw new IllegalArgumentException("State must be either " + stateToString(STATE_FLAT)
- + ", " + stateToString(STATE_HALF_OPENED) + ", or "
- + stateToString(STATE_FLIPPED));
+ + " or " + stateToString(STATE_HALF_OPENED));
}
}
@@ -312,8 +302,6 @@
switch (state) {
case STATE_FLAT:
return "FLAT";
- case STATE_FLIPPED:
- return "FLIPPED";
case STATE_HALF_OPENED:
return "HALF_OPENED";
default:
diff --git a/window/window/src/main/java/androidx/window/SidecarAdapter.java b/window/window/src/main/java/androidx/window/SidecarAdapter.java
index d93fcbd..31dd6c8 100644
--- a/window/window/src/main/java/androidx/window/SidecarAdapter.java
+++ b/window/window/src/main/java/androidx/window/SidecarAdapter.java
@@ -274,10 +274,8 @@
switch (devicePosture) {
case SidecarDeviceState.POSTURE_CLOSED:
case SidecarDeviceState.POSTURE_UNKNOWN:
- return null;
case SidecarDeviceState.POSTURE_FLIPPED:
- state = FoldingFeature.STATE_FLIPPED;
- break;
+ return null;
case SidecarDeviceState.POSTURE_HALF_OPENED:
state = FoldingFeature.STATE_HALF_OPENED;
break;
diff --git a/window/window/src/main/java/androidx/window/WindowBackend.java b/window/window/src/main/java/androidx/window/WindowBackend.java
index b45817b..3eb4869 100644
--- a/window/window/src/main/java/androidx/window/WindowBackend.java
+++ b/window/window/src/main/java/androidx/window/WindowBackend.java
@@ -17,7 +17,6 @@
package androidx.window;
import android.app.Activity;
-import android.content.Context;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
@@ -38,27 +37,8 @@
@NonNull Consumer<WindowLayoutInfo> callback);
/**
- * Registers a callback for layout changes of the window for {@link Activity} associated with
- * the supplied {@link Context}. Must be called only after the it is attached to the window.
- * @deprecated will be removed in the next alpha.
- */
- @Deprecated
- void registerLayoutChangeCallback(@NonNull Context context, @NonNull Executor executor,
- @NonNull Consumer<WindowLayoutInfo> callback);
-
- /**
* Unregisters a callback for window layout changes of the {@link Activity} window.
*/
void unregisterLayoutChangeCallback(@NonNull Consumer<WindowLayoutInfo> callback);
- /**
- * Registers a callback for device state changes.
- */
- void registerDeviceStateChangeCallback(@NonNull Executor executor,
- @NonNull Consumer<DeviceState> callback);
-
- /**
- * Unregisters a callback for device state changes.
- */
- void unregisterDeviceStateChangeCallback(@NonNull Consumer<DeviceState> callback);
}
diff --git a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
index ac8d77a..48f0b1e 100644
--- a/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
+++ b/window/window/src/test/java/androidx/window/ExtensionWindowBackendUnitTest.java
@@ -16,12 +16,8 @@
package androidx.window;
-import static com.google.common.truth.Truth.assertThat;
-import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@@ -35,9 +31,6 @@
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
-import com.google.common.collect.BoundType;
-import com.google.common.collect.Range;
-
import org.junit.Before;
import org.junit.Test;
@@ -68,33 +61,6 @@
}
@Test
- public void testRegisterDeviceStateChangeCallback_noExtension() {
- // Verify method with extension
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = null;
- SimpleConsumer<DeviceState> simpleConsumer = new SimpleConsumer<>();
-
- backend.registerDeviceStateChangeCallback(directExecutor(), simpleConsumer);
-
- DeviceState deviceState = simpleConsumer.lastValue();
- assertNotNull(deviceState);
- assertThat(deviceState.getPosture()).isIn(Range.range(
- DeviceState.POSTURE_UNKNOWN, BoundType.CLOSED,
- DeviceState.POSTURE_MAX_KNOWN, BoundType.CLOSED));
- DeviceState initialLastReportedState = backend.mLastReportedDeviceState;
-
- // Verify method without extension
- backend.mWindowExtension = null;
- SimpleConsumer<DeviceState> noExtensionConsumer = new SimpleConsumer<>();
- backend.registerDeviceStateChangeCallback(directExecutor(), noExtensionConsumer);
- deviceState = noExtensionConsumer.lastValue();
- assertNotNull(deviceState);
- assertEquals(DeviceState.POSTURE_UNKNOWN, deviceState.getPosture());
- // Verify that last reported state does not change when using the getter
- assertEquals(initialLastReportedState, backend.mLastReportedDeviceState);
- }
-
- @Test
public void testRegisterLayoutChangeCallback() {
ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
@@ -187,113 +153,6 @@
verify(backend.mWindowExtension).onWindowLayoutChangeListenerRemoved(activity);
}
- @Test
- public void testRegisterDeviceChangeCallback() {
- ExtensionInterfaceCompat mockInterface = mock(ExtensionInterfaceCompat.class);
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mockInterface;
-
- // Check registering the device state change callback
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
- assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(false));
-
- // Check unregistering the device state change callback
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(eq(true));
- }
-
- @Test
- public void testDeviceChangeCallback() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check that callbacks from the extension are propagated correctly
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- DeviceState deviceState = newTestDeviceState();
- ExtensionWindowBackend.ExtensionListenerImpl backendListener =
- backend.new ExtensionListenerImpl();
- backendListener.onDeviceStateChanged(deviceState);
-
- verify(consumer, times(1)).accept(eq(deviceState));
- assertEquals(deviceState, backend.mLastReportedDeviceState);
-
- // Test that the same value wouldn't be reported again
- backendListener.onDeviceStateChanged(deviceState);
- verify(consumer, times(1)).accept(any());
- }
-
- @Test
- public void testDeviceChangeChangeCallback_callsExtensionOnce() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check registering the layout change callback
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- backend.registerDeviceStateChangeCallback(Runnable::run, mock(DeviceStateConsumer.class));
-
- assertEquals(2, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(false);
-
- // Check unregistering the layout change callback
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertEquals(1, backend.mDeviceStateChangeCallbacks.size());
- verify(backend.mWindowExtension, times(0))
- .onDeviceStateListenersChanged(true);
- }
-
- @Test
- public void testDeviceChangeChangeCallback_clearListeners() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
-
- // Check registering the layout change callback
- Consumer<DeviceState> firstConsumer = mock(DeviceStateConsumer.class);
- Consumer<DeviceState> secondConsumer = mock(DeviceStateConsumer.class);
- backend.registerDeviceStateChangeCallback(Runnable::run, firstConsumer);
- backend.registerDeviceStateChangeCallback(Runnable::run, secondConsumer);
-
- // Check unregistering the layout change callback
- backend.unregisterDeviceStateChangeCallback(firstConsumer);
- backend.unregisterDeviceStateChangeCallback(secondConsumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- verify(backend.mWindowExtension).onDeviceStateListenersChanged(true);
- }
-
- @Test
- public void testDeviceChangeCallback_relayLastEmittedValue() {
- DeviceState expectedState = newTestDeviceState();
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
- backend.mWindowExtension = mock(ExtensionInterfaceCompat.class);
- backend.mLastReportedDeviceState = expectedState;
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
-
- verify(consumer).accept(expectedState);
- }
-
- @Test
- public void testDeviceChangeCallback_clearLastEmittedValue() {
- ExtensionWindowBackend backend = ExtensionWindowBackend.getInstance(mContext);
- Consumer<DeviceState> consumer = mock(DeviceStateConsumer.class);
-
- backend.registerDeviceStateChangeCallback(Runnable::run, consumer);
- backend.unregisterDeviceStateChangeCallback(consumer);
-
- assertTrue(backend.mDeviceStateChangeCallbacks.isEmpty());
- assertNull(backend.mLastReportedDeviceState);
- }
-
private static WindowLayoutInfo newTestWindowLayoutInfo() {
WindowLayoutInfo.Builder builder = new WindowLayoutInfo.Builder();
return builder.build();
diff --git a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
index bc14ba6..e889f98 100644
--- a/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
+++ b/window/window/src/testUtil/java/androidx/window/TestFoldingFeatureUtil.java
@@ -17,7 +17,6 @@
package androidx.window;
import static androidx.window.FoldingFeature.STATE_FLAT;
-import static androidx.window.FoldingFeature.STATE_FLIPPED;
import static androidx.window.FoldingFeature.STATE_HALF_OPENED;
import static androidx.window.FoldingFeature.TYPE_FOLD;
import static androidx.window.FoldingFeature.TYPE_HINGE;
@@ -93,7 +92,6 @@
states.add(new FoldingFeature(bounds, type, STATE_FLAT));
states.add(new FoldingFeature(bounds, type, STATE_HALF_OPENED));
- states.add(new FoldingFeature(bounds, type, STATE_FLIPPED));
return states;
}
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 87093e5..a84b95f 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -68,6 +68,7 @@
implementation(project(":work:work-runtime-ktx"))
implementation(project(":work:work-multiprocess"))
implementation(project(":work:work-gcm"))
+ implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
implementation("androidx.arch.core:core-runtime:2.1.0")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation(MATERIAL)
diff --git a/work/integration-tests/testapp/src/main/AndroidManifest.xml b/work/integration-tests/testapp/src/main/AndroidManifest.xml
index 8eb67e8..17f5540 100644
--- a/work/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/work/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -16,6 +16,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.work.integration.testapp">
+
<application
android:name=".TestApplication"
android:allowBackup="true"
@@ -37,14 +38,26 @@
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
- <service
- android:name=".RemoteService"
- android:exported="false"
- android:process=":remote" />
+
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
+
+ <service
+ android:name="androidx.work.multiprocess.RemoteWorkerService"
+ android:exported="false"
+ android:process=":worker1" />
+
+ <service
+ android:name=".RemoteWorkerService2"
+ android:exported="false"
+ android:process=":worker2" />
+
+ <service
+ android:name=".RemoteService"
+ android:exported="false"
+ android:process=":remote" />
</application>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
index f792754..f65cae7 100644
--- a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/MainActivity.java
@@ -18,6 +18,8 @@
import static androidx.work.ExistingWorkPolicy.KEEP;
import static androidx.work.ExistingWorkPolicy.REPLACE;
+import static androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME;
+import static androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME;
import android.app.PendingIntent;
import android.app.job.JobInfo;
@@ -35,6 +37,7 @@
import android.widget.EditText;
import android.widget.Toast;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.Observer;
@@ -52,6 +55,7 @@
import androidx.work.impl.workers.ConstraintTrackingWorker;
import androidx.work.integration.testapp.imageprocessing.ImageProcessingActivity;
import androidx.work.integration.testapp.sherlockholmes.AnalyzeSherlockHolmesActivity;
+import androidx.work.multiprocess.RemoteWorkerService;
import java.util.ArrayList;
import java.util.List;
@@ -62,6 +66,7 @@
*/
public class MainActivity extends AppCompatActivity {
+ private static final String PACKAGE_NAME = "androidx.work.integration.testapp";
private static final String TAG = "MainActivity";
private static final String CONSTRAINT_TRACKING_TAG = "ConstraintTrackingWorker";
private static final String UNIQUE_WORK_NAME = "importantUniqueWork";
@@ -504,6 +509,36 @@
}
});
+ findViewById(R.id.enqueue_remote_worker_1).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String serviceName = RemoteWorkerService.class.getName();
+ ComponentName componentName = new ComponentName(PACKAGE_NAME, serviceName);
+ OneTimeWorkRequest request = buildOneTimeWorkRemoteWorkRequest(componentName);
+ WorkManager.getInstance(MainActivity.this)
+ .enqueue(request);
+ }
+ });
+
+ findViewById(R.id.enqueue_remote_worker_2).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String serviceName = RemoteWorkerService2.class.getName();
+ ComponentName componentName = new ComponentName(PACKAGE_NAME, serviceName);
+ OneTimeWorkRequest request = buildOneTimeWorkRemoteWorkRequest(componentName);
+ WorkManager.getInstance(MainActivity.this)
+ .enqueue(request);
+ }
+ });
+
+ findViewById(R.id.cancel_remote_workers).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ WorkManager.getInstance(MainActivity.this)
+ .cancelAllWorkByTag(RemoteWorker.class.getName());
+ }
+ });
+
findViewById(R.id.crash_app).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
@@ -540,6 +575,17 @@
} else {
hundredJobExceptionButton.setVisibility(View.GONE);
}
+ }
+ @NonNull
+ OneTimeWorkRequest buildOneTimeWorkRemoteWorkRequest(@NonNull ComponentName componentName) {
+ Data data = new Data.Builder()
+ .putString(ARGUMENT_PACKAGE_NAME, componentName.getPackageName())
+ .putString(ARGUMENT_CLASS_NAME, componentName.getClassName())
+ .build();
+
+ return new OneTimeWorkRequest.Builder(RemoteWorker.class)
+ .setInputData(data)
+ .build();
}
}
diff --git a/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
new file mode 100644
index 0000000..4e01eb24
--- /dev/null
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorker.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.work.integration.testapp
+
+import android.content.Context
+import android.util.Log
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.work.WorkerParameters
+import androidx.work.multiprocess.RemoteListenableWorker
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+class RemoteWorker(private val context: Context, private val parameters: WorkerParameters) :
+ RemoteListenableWorker(context, parameters) {
+ private var job: Job? = null
+ override fun startRemoteWork(): ListenableFuture<Result> {
+ return CallbackToFutureAdapter.getFuture { completer ->
+ Log.d(TAG, "Starting Remote Worker.")
+ val scope = CoroutineScope(Dispatchers.Default)
+
+ job = scope.launch {
+ delay(30 * 1000)
+ }
+ job?.invokeOnCompletion {
+ Log.d(TAG, "Done.")
+ completer.set(Result.success())
+ }
+ }
+ }
+
+ override fun onStopped() {
+ job?.cancel()
+ }
+
+ companion object {
+ private const val TAG = "RemoteWorker"
+ }
+}
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
similarity index 76%
copy from compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
copy to work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
index 5ece3c2..256bc17 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/MaterialCatalog.kt
+++ b/work/integration-tests/testapp/src/main/java/androidx/work/integration/testapp/RemoteWorkerService2.kt
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-package androidx.compose.material.catalog
+package androidx.work.integration.testapp
-import androidx.compose.integration.demos.common.ActivityDemo
+import androidx.work.multiprocess.RemoteWorkerService
-val MaterialCatalog = ActivityDemo("Material Catalog", CatalogActivity::class)
+class RemoteWorkerService2 : RemoteWorkerService()
\ No newline at end of file
diff --git a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
index b5fd175..123781e 100644
--- a/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/work/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -287,6 +287,30 @@
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"/>
+ <Button android:text="@string/run_remote_worker_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/enqueue_remote_worker_1"
+ android:layout_marginTop="12dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginStart="16dp"/>
+
+ <Button android:text="@string/run_remote_worker_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/enqueue_remote_worker_2"
+ android:layout_marginTop="12dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginStart="16dp"/>
+
+ <Button android:text="@string/cancel_remote_workers"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/cancel_remote_workers"
+ android:layout_marginTop="12dp"
+ android:layout_marginLeft="16dp"
+ android:layout_marginStart="16dp"/>
+
<Button android:text="@string/crash_app"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/work/integration-tests/testapp/src/main/res/values/strings.xml b/work/integration-tests/testapp/src/main/res/values/strings.xml
index 11703b4..3543341 100644
--- a/work/integration-tests/testapp/src/main/res/values/strings.xml
+++ b/work/integration-tests/testapp/src/main/res/values/strings.xml
@@ -34,6 +34,9 @@
<string name="cancel_work_tag_multiprocess">Cancel Work By Tag (Multi-process)</string>
<string name="cancel_all_work_multiprocess">Cancel All Work (Multi-process)</string>
<string name="query_work_multiprocess">Query Work (Multi-process)</string>
+ <string name="run_remote_worker_1">Run Remote Worker 1</string>
+ <string name="run_remote_worker_2">Run Remote Worker 2</string>
+ <string name="cancel_remote_workers">Cancel Remote Workers</string>
<string name="crash_app">Crash App</string>
<string name="create_hundred_job_exception">Create 100 Job Exception</string>
<string name="keep">Use KEEP</string>
diff --git a/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java b/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
index 39094ae..43acc2e 100644
--- a/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
+++ b/work/workmanager-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
@@ -17,9 +17,13 @@
package androidx.work.impl.background.gcm;
+import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
+import android.os.Build;
+
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.work.Constraints;
@@ -98,6 +102,13 @@
break;
case NOT_REQUIRED:
builder.setRequiredNetwork(Task.NETWORK_STATE_ANY);
+ break;
+ default:
+ if (Build.VERSION.SDK_INT >= 30) {
+ if (networkType == TEMPORARILY_UNMETERED) {
+ builder.setRequiredNetwork(Task.NETWORK_STATE_ANY);
+ }
+ }
}
// Charging constraints
diff --git a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
index af939ff..9071e03 100644
--- a/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
+++ b/work/workmanager-inspection/src/androidTest/java/androidx/work/inspection/BasicTest.kt
@@ -17,6 +17,7 @@
package androidx.work.inspection
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.work.inspection.WorkManagerInspectorProtocol.Command
import androidx.work.inspection.WorkManagerInspectorProtocol.TrackWorkManagerCommand
@@ -38,6 +39,7 @@
testEnvironment.assertNoQueuedEvents()
}
+ @FlakyTest
@Test
fun sendUnsetCommand() = runBlocking {
testEnvironment.sendCommand(Command.getDefaultInstance())
diff --git a/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt b/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
index c8a8c1b..8519043 100644
--- a/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
+++ b/work/workmanager-lint/src/main/java/androidx/work/lint/RemoveWorkManagerInitializerDetector.kt
@@ -46,7 +46,7 @@
companion object {
- private const val DESCRIPTION = "Remove androidx.work.impl.WorkManagerInitializer from " +
+ private const val DESCRIPTION = "Remove androidx.work.WorkManagerInitializer from " +
"your AndroidManifest.xml when using on-demand initialization."
val ISSUE = Issue.create(
@@ -105,7 +105,7 @@
val metadataElements = element.getElementsByTagName("meta-data")
val metadata = metadataElements.find { node ->
val name = node.attributes.getNamedItemNS(ANDROID_URI, ATTR_NAME)?.textContent
- name == "androidx.work.impl.WorkManagerInitializer"
+ name == "androidx.work.WorkManagerInitializer"
}
if (metadata != null && !removedDefaultInitializer) {
location = context.getLocation(metadata)
diff --git a/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt b/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
index 7a6cb1d..309580d 100644
--- a/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
+++ b/work/workmanager-lint/src/test/java/androidx/work/lint/RemoveWorkManagerInitializerDetectorTest.kt
@@ -159,7 +159,7 @@
.run()
.expect(
"""
- project0: Error: Remove androidx.work.impl.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
+ project0: Error: Remove androidx.work.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
1 errors, 0 warnings
""".trimIndent()
)
@@ -196,7 +196,7 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="com.example.workmanager-init">
<meta-data
- android:name="androidx.work.impl.WorkManagerInitializer"
+ android:name="androidx.work.WorkManagerInitializer"
android:value="@string/androidx_startup" />
</provider>
</application>
@@ -216,7 +216,7 @@
.run()
.expect(
"""
- AndroidManifest.xml:8: Error: Remove androidx.work.impl.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
+ AndroidManifest.xml:8: Error: Remove androidx.work.WorkManagerInitializer from your AndroidManifest.xml when using on-demand initialization. [RemoveWorkManagerInitializer]
<meta-data
^
1 errors, 0 warnings
diff --git a/work/workmanager-multiprocess/api/current.txt b/work/workmanager-multiprocess/api/current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/current.txt
+++ b/work/workmanager-multiprocess/api/current.txt
@@ -1 +1,18 @@
// Signature format: 4.0
+package androidx.work.multiprocess {
+
+ public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+ ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+ field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+ }
+
+ public class RemoteWorkerService extends android.app.Service {
+ ctor public RemoteWorkerService();
+ method public android.os.IBinder? onBind(android.content.Intent);
+ }
+
+}
+
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
+++ b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
@@ -1 +1,18 @@
// Signature format: 4.0
+package androidx.work.multiprocess {
+
+ public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+ ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+ field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+ }
+
+ public class RemoteWorkerService extends android.app.Service {
+ ctor public RemoteWorkerService();
+ method public android.os.IBinder? onBind(android.content.Intent);
+ }
+
+}
+
diff --git a/work/workmanager-multiprocess/api/restricted_current.txt b/work/workmanager-multiprocess/api/restricted_current.txt
index e6f50d0..bcdaaae 100644
--- a/work/workmanager-multiprocess/api/restricted_current.txt
+++ b/work/workmanager-multiprocess/api/restricted_current.txt
@@ -1 +1,18 @@
// Signature format: 4.0
+package androidx.work.multiprocess {
+
+ public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+ ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+ method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+ method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+ field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+ field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+ }
+
+ public class RemoteWorkerService extends android.app.Service {
+ ctor public RemoteWorkerService();
+ method public android.os.IBinder? onBind(android.content.Intent);
+ }
+
+}
+
diff --git a/work/workmanager-multiprocess/build.gradle b/work/workmanager-multiprocess/build.gradle
index 94d9a0c..a81a41f 100644
--- a/work/workmanager-multiprocess/build.gradle
+++ b/work/workmanager-multiprocess/build.gradle
@@ -41,6 +41,7 @@
dependencies {
api project(":work:work-runtime")
+ implementation("androidx.room:room-runtime:2.2.5")
api(GUAVA_LISTENABLE_FUTURE)
androidTestImplementation(KOTLIN_STDLIB)
androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml b/work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
similarity index 64%
rename from compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml
rename to work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
index 6e4f4af..9721117 100644
--- a/compose/ui/ui-viewbinding/src/androidTest/AndroidManifest.xml
+++ b/work/workmanager-multiprocess/src/androidTest/AndroidManifest.xml
@@ -1,6 +1,5 @@
-<?xml version="1.0" encoding="utf-8"?>
<!--
- Copyright 2020 The Android Open Source Project
+ 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.
@@ -14,4 +13,11 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<manifest package="androidx.compose.ui.viewinterop" />
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="androidx.work.multiprocess.test">
+
+ <application>
+ <service android:name="androidx.work.multiprocess.RemoteWorkerService" />
+ </application>
+
+</manifest>
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt
new file mode 100644
index 0000000..ab84364
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ListenableWorkerImplClientTest.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.work.multiprocess
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Build
+import android.os.IBinder
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.utils.SerialExecutor
+import androidx.work.impl.utils.futures.SettableFuture
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class ListenableWorkerImplClientTest {
+ private lateinit var mContext: Context
+ private lateinit var mWorkManager: WorkManagerImpl
+ private lateinit var mExecutor: Executor
+ private lateinit var mClient: ListenableWorkerImplClient
+
+ @Before
+ public fun setUp() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ mContext = mock(Context::class.java)
+ mWorkManager = mock(WorkManagerImpl::class.java)
+ `when`(mContext.applicationContext).thenReturn(mContext)
+ mExecutor = Executor {
+ it.run()
+ }
+
+ val taskExecutor = mock(TaskExecutor::class.java)
+ `when`(taskExecutor.backgroundExecutor).thenReturn(SerialExecutor(mExecutor))
+ `when`(mWorkManager.workTaskExecutor).thenReturn(taskExecutor)
+ mClient = ListenableWorkerImplClient(mContext, mExecutor)
+ }
+
+ @Test
+ @MediumTest
+ public fun failGracefullyWhenBindFails() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+ `when`(
+ mContext.bindService(
+ any(Intent::class.java),
+ any(ServiceConnection::class.java),
+ anyInt()
+ )
+ ).thenReturn(false)
+ val componentName = ComponentName("packageName", "className")
+ var exception: Throwable? = null
+ try {
+ mClient.getListenableWorkerImpl(componentName).get()
+ } catch (throwable: Throwable) {
+ exception = throwable
+ }
+ assertNotNull(exception)
+ val message = exception?.cause?.message ?: ""
+ assertTrue(message.contains("Unable to bind to service"))
+ }
+
+ @Test
+ @MediumTest
+ @Suppress("UNCHECKED_CAST")
+ public fun cleanUpWhenDispatcherFails() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val binder = mock(IBinder::class.java)
+ val remoteDispatcher =
+ mock(RemoteDispatcher::class.java) as RemoteDispatcher<IListenableWorkerImpl>
+ val remoteStub = mock(IListenableWorkerImpl::class.java)
+ val callback = spy(RemoteCallback())
+ val message = "Something bad happened"
+ `when`(remoteDispatcher.execute(remoteStub, callback)).thenThrow(RuntimeException(message))
+ `when`(remoteStub.asBinder()).thenReturn(binder)
+ val session = SettableFuture.create<IListenableWorkerImpl>()
+ session.set(remoteStub)
+ var exception: Throwable? = null
+ try {
+ mClient.execute(session, remoteDispatcher, callback).get()
+ } catch (throwable: Throwable) {
+ exception = throwable
+ }
+ assertNotNull(exception)
+ verify(callback).onFailure(message)
+ }
+
+ @Test
+ @MediumTest
+ @Suppress("UNCHECKED_CAST")
+ public fun cleanUpWhenSessionIsInvalid() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val remoteDispatcher =
+ mock(RemoteDispatcher::class.java) as RemoteDispatcher<IListenableWorkerImpl>
+ val callback = spy(RemoteCallback())
+ val session = SettableFuture.create<IListenableWorkerImpl>()
+ session.setException(RuntimeException("Something bad happened"))
+ var exception: Throwable? = null
+ try {
+ mClient.execute(session, remoteDispatcher, callback).get()
+ } catch (throwable: Throwable) {
+ exception = throwable
+ }
+ assertNotNull(exception)
+ verify(callback).onFailure(anyString())
+ }
+
+ @Test
+ @MediumTest
+ public fun cleanUpOnSuccessfulDispatch() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val binder = mock(IBinder::class.java)
+ val remoteDispatcher = RemoteDispatcher<IListenableWorkerImpl> { _, callback ->
+ callback.onSuccess(ByteArray(0))
+ }
+ val remoteStub = mock(IListenableWorkerImpl::class.java)
+ val callback = spy(RemoteCallback())
+ `when`(remoteStub.asBinder()).thenReturn(binder)
+ val session = SettableFuture.create<IListenableWorkerImpl>()
+ session.set(remoteStub)
+ var exception: Throwable? = null
+ try {
+ mClient.execute(session, remoteDispatcher, callback).get()
+ } catch (throwable: Throwable) {
+ exception = throwable
+ }
+ assertNull(exception)
+ verify(callback).onSuccess(any())
+ }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt
new file mode 100644
index 0000000..4f281f3
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableDataTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.work.Data
+import androidx.work.multiprocess.parcelable.ParcelConverters
+import androidx.work.multiprocess.parcelable.ParcelableData
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+public class ParcelableDataTest {
+
+ @Test
+ @SmallTest
+ public fun testParcelableData() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val data = createData()
+ val parcelableData = ParcelableData(data)
+ val parcelled: ParcelableData =
+ ParcelConverters.unmarshall(
+ ParcelConverters.marshall(parcelableData),
+ ParcelableData.CREATOR
+ )
+ assertEquals(data, parcelled.data)
+ }
+
+ private fun createData(): Data {
+ val map = mutableMapOf<String, Any?>()
+ map["byte"] = 1.toByte()
+ map["boolean"] = false
+ map["int"] = 1
+ map["long"] = 10L
+ map["float"] = 99f
+ map["double"] = 99.0
+ map["string"] = "two"
+ map["byte array"] = byteArrayOf(1, 2, 3)
+ map["boolean array"] = booleanArrayOf(true, false, true)
+ map["int array"] = intArrayOf(1, 2, 3)
+ map["long array"] = longArrayOf(1L, 2L, 3L)
+ map["float array"] = floatArrayOf(1f, 2f, 3f)
+ map["double array"] = doubleArrayOf(1.0, 2.0, 3.0)
+ map["string array"] = listOf("a", "b", "c").toTypedArray()
+ map["null"] = null
+ val dataBuilder = Data.Builder()
+ dataBuilder.putAll(map)
+ return dataBuilder.build()
+ }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
index ae64f39..29d7f79 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkContinuationImplTest.kt
@@ -17,6 +17,7 @@
package androidx.work.multiprocess
import android.content.Context
+import android.os.Build
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.test.core.app.ApplicationProvider
@@ -50,6 +51,11 @@
@Before
public fun setUp() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
context = ApplicationProvider.getApplicationContext<Context>()
val taskExecutor = object : TaskExecutor() {
override fun executeOnDiskIO(runnable: Runnable) {
@@ -105,6 +111,11 @@
@Test
@MediumTest
public fun basicContinuationTest() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val continuation = workManager.beginWith(listOf(first)).then(second)
@@ -115,6 +126,11 @@
@Test
@MediumTest
public fun continuationTests2() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val third = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
@@ -126,6 +142,11 @@
@Test
@MediumTest
public fun continuationTest3() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val continuation = workManager.beginUniqueWork(
@@ -138,6 +159,11 @@
@Test
@MediumTest
public fun continuationTest4() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val continuation = workManager.beginUniqueWork(
@@ -154,6 +180,11 @@
@Test
@MediumTest
public fun combineContinuationTests() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val first = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val second = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
val third = OneTimeWorkRequest.Builder(TestWorker::class.java).build()
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
index 5b42c27..b77d969 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkInfoTest.kt
@@ -16,6 +16,7 @@
package androidx.work.multiprocess
+import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.work.Data
@@ -31,9 +32,16 @@
@RunWith(AndroidJUnit4::class)
public class ParcelableWorkInfoTest {
+ // Setting the minSdkVersion to 27 otherwise we end up with SIGSEGVs.
+
@Test
@SmallTest
public fun converterTest1() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val workInfo = WorkInfo(
UUID.randomUUID(),
WorkInfo.State.ENQUEUED,
@@ -48,6 +56,11 @@
@Test
@SmallTest
public fun converterTest2() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val data = Data.Builder()
.put("test", "testString")
.put("int", 10)
@@ -67,6 +80,11 @@
@Test
@SmallTest
public fun arrayConverterTest1() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val data = Data.Builder()
.put("test", "testString")
.put("int", 10)
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
index c3dfbd5..64a7a50 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkQueryTest.kt
@@ -16,6 +16,7 @@
package androidx.work.multiprocess
+import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.work.WorkInfo
@@ -33,6 +34,11 @@
@Test
@SmallTest
public fun converterTest1() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val workQuery = WorkQuery.Builder.fromUniqueWorkNames(listOf("name1"))
.addTags(listOf("tag1", "tag2"))
.addIds(listOf(UUID.randomUUID()))
@@ -45,6 +51,11 @@
@Test
@SmallTest
public fun converterTest2() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val workQuery = WorkQuery.Builder.fromUniqueWorkNames(listOf("name1"))
.build()
@@ -54,6 +65,11 @@
@Test
@SmallTest
public fun converterTest3() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val workQuery = WorkQuery.Builder.fromTags(listOf("tag1", "tag2"))
.build()
@@ -63,6 +79,11 @@
@Test
@SmallTest
public fun converterTest4() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val workQuery = WorkQuery.Builder.fromStates(listOf(WorkInfo.State.ENQUEUED))
.build()
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
index d95f76b..d98d45f 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/ParcelableWorkRequestConvertersTest.kt
@@ -17,8 +17,8 @@
package androidx.work.multiprocess
import android.net.Uri
+import android.os.Build
import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.work.BackoffPolicy
import androidx.work.Constraints
@@ -39,6 +39,11 @@
@Test
@SmallTest
public fun converterTest1() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
.addTag("Test Worker")
.keepResultsForAtLeast(1, TimeUnit.DAYS)
@@ -49,6 +54,11 @@
@Test
@SmallTest
public fun converterTest2() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
.setInitialDelay(1, TimeUnit.HOURS)
.setInputData(
@@ -69,8 +79,12 @@
@Test
@SmallTest
- @SdkSuppress(minSdkVersion = 24)
public fun converterTest3() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val uri = Uri.parse("test://foo")
val request = OneTimeWorkRequest.Builder(TestWorker::class.java)
.setInitialDelay(1, TimeUnit.HOURS)
@@ -96,6 +110,11 @@
@Test
@SmallTest
public fun converterTest4() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val requests = mutableListOf<WorkRequest>()
repeat(10) {
requests += OneTimeWorkRequest.Builder(TestWorker::class.java)
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
new file mode 100644
index 0000000..fcc9575
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.work.multiprocess
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always fails.
+ */
+public class RemoteFailureWorker(context: Context, workerParameters: WorkerParameters) :
+ RemoteListenableWorker(context, workerParameters) {
+ override fun startRemoteWork(): ListenableFuture<Result> {
+ val future = SettableFuture.create<Result>()
+ val result = Result.failure(outputData())
+ future.set(result)
+ return future
+ }
+
+ public companion object {
+ public fun outputData(): Data {
+ return Data.Builder()
+ .put("output_1", 1)
+ .put("output_2,", "test")
+ .build()
+ }
+ }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
new file mode 100644
index 0000000..d490a33
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.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.work.multiprocess
+
+import android.content.Context
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.Configuration
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkRequest
+import androidx.work.impl.Processor
+import androidx.work.impl.Scheduler
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkerWrapper
+import androidx.work.impl.foreground.ForegroundProcessor
+import androidx.work.impl.utils.SerialExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_CLASS_NAME
+import androidx.work.multiprocess.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class RemoteListenableWorkerTest {
+ private lateinit var mConfiguration: Configuration
+ private lateinit var mTaskExecutor: TaskExecutor
+ private lateinit var mScheduler: Scheduler
+ private lateinit var mProcessor: Processor
+ private lateinit var mForegroundProcessor: ForegroundProcessor
+ private lateinit var mWorkManager: WorkManagerImpl
+ private lateinit var mExecutor: Executor
+
+ // Necessary for the reified function
+ public lateinit var mContext: Context
+ public lateinit var mDatabase: WorkDatabase
+
+ @Before
+ public fun setUp() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ mContext = InstrumentationRegistry.getInstrumentation().context
+ mExecutor = Executor {
+ it.run()
+ }
+ mConfiguration = Configuration.Builder()
+ .setExecutor(mExecutor)
+ .setTaskExecutor(mExecutor)
+ .build()
+ mTaskExecutor = mock(TaskExecutor::class.java)
+ `when`(mTaskExecutor.backgroundExecutor).thenReturn(SerialExecutor(mExecutor))
+ `when`(mTaskExecutor.mainThreadExecutor).thenReturn(mExecutor)
+ mScheduler = mock(Scheduler::class.java)
+ mForegroundProcessor = mock(ForegroundProcessor::class.java)
+ mWorkManager = mock(WorkManagerImpl::class.java)
+ mDatabase = WorkDatabase.create(mContext, mExecutor, true)
+ val schedulers = listOf(mScheduler)
+ // Processor
+ mProcessor = Processor(mContext, mConfiguration, mTaskExecutor, mDatabase, schedulers)
+ // WorkManagerImpl
+ `when`(mWorkManager.configuration).thenReturn(mConfiguration)
+ `when`(mWorkManager.workTaskExecutor).thenReturn(mTaskExecutor)
+ `when`(mWorkManager.workDatabase).thenReturn(mDatabase)
+ `when`(mWorkManager.schedulers).thenReturn(schedulers)
+ `when`(mWorkManager.processor).thenReturn(mProcessor)
+ WorkManagerImpl.setDelegate(mWorkManager)
+ }
+
+ @Test
+ @MediumTest
+ public fun testRemoteSuccessWorker() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val request = buildRequest<RemoteSuccessWorker>()
+ val wrapper = buildWrapper(request)
+ wrapper.run()
+ wrapper.future.get()
+ val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+ assertEquals(workSpec.state, WorkInfo.State.SUCCEEDED)
+ assertEquals(workSpec.output, RemoteSuccessWorker.outputData())
+ }
+
+ @Test
+ @MediumTest
+ public fun testRemoteFailureWorker() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val request = buildRequest<RemoteFailureWorker>()
+ val wrapper = buildWrapper(request)
+ wrapper.run()
+ wrapper.future.get()
+ val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+ assertEquals(workSpec.state, WorkInfo.State.FAILED)
+ assertEquals(workSpec.output, RemoteFailureWorker.outputData())
+ }
+
+ @Test
+ @MediumTest
+ public fun testRemoteRetryWorker() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val request = buildRequest<RemoteRetryWorker>()
+ val wrapper = buildWrapper(request)
+ wrapper.run()
+ wrapper.future.get()
+ val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+ assertEquals(workSpec.state, WorkInfo.State.ENQUEUED)
+ }
+
+ public inline fun <reified T : RemoteListenableWorker> buildRequest(): OneTimeWorkRequest {
+ val inputData = Data.Builder()
+ .putString(ARGUMENT_PACKAGE_NAME, mContext.packageName)
+ .putString(ARGUMENT_CLASS_NAME, RemoteWorkerService::class.java.name)
+ .build()
+
+ val request = OneTimeWorkRequest.Builder(T::class.java)
+ .setInputData(inputData)
+ .build()
+
+ mDatabase.workSpecDao().insertWorkSpec(request.workSpec)
+ return request
+ }
+
+ public fun buildWrapper(request: WorkRequest): WorkerWrapper {
+ return WorkerWrapper.Builder(
+ mContext,
+ mConfiguration,
+ mTaskExecutor,
+ mForegroundProcessor,
+ mDatabase,
+ request.stringId
+ ).build()
+ }
+}
\ No newline at end of file
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
new file mode 100644
index 0000000..68afb87b
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.work.multiprocess
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always retries.
+ */
+public class RemoteRetryWorker(context: Context, workerParameters: WorkerParameters) :
+ RemoteListenableWorker(context, workerParameters) {
+ override fun startRemoteWork(): ListenableFuture<Result> {
+ val future = SettableFuture.create<Result>()
+ val result = Result.retry()
+ future.set(result)
+ return future
+ }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
new file mode 100644
index 0000000..810f3ef
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.work.multiprocess
+
+import android.content.Context
+import androidx.work.Data
+import androidx.work.WorkerParameters
+import androidx.work.impl.utils.futures.SettableFuture
+import com.google.common.util.concurrent.ListenableFuture
+
+/**
+ * A Remote Listenable Worker which always succeeds.
+ */
+public class RemoteSuccessWorker(context: Context, workerParameters: WorkerParameters) :
+ RemoteListenableWorker(context, workerParameters) {
+ override fun startRemoteWork(): ListenableFuture<Result> {
+ val future = SettableFuture.create<Result>()
+ val result = Result.success(outputData())
+ future.set(result)
+ return future
+ }
+
+ public companion object {
+ public fun outputData(): Data {
+ return Data.Builder()
+ .put("output_1", 1)
+ .put("output_2,", "test")
+ .build()
+ }
+ }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
index fba4ec3..f74b7cf 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteWorkManagerClientTest.kt
@@ -53,6 +53,11 @@
@Before
public fun setUp() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
mContext = mock(Context::class.java)
mWorkManager = mock(WorkManagerImpl::class.java)
`when`(mContext.applicationContext).thenReturn(mContext)
@@ -94,9 +99,16 @@
@Test
@MediumTest
+ @Suppress("UNCHECKED_CAST")
public fun cleanUpWhenDispatcherFails() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val binder = mock(IBinder::class.java)
- val remoteDispatcher = mock(RemoteWorkManagerClient.RemoteDispatcher::class.java)
+ val remoteDispatcher =
+ mock(RemoteDispatcher::class.java) as RemoteDispatcher<IWorkManagerImpl>
val remoteStub = mock(IWorkManagerImpl::class.java)
val callback = spy(RemoteCallback())
val message = "Something bad happened"
@@ -117,8 +129,15 @@
@Test
@MediumTest
+ @Suppress("UNCHECKED_CAST")
public fun cleanUpWhenSessionIsInvalid() {
- val remoteDispatcher = mock(RemoteWorkManagerClient.RemoteDispatcher::class.java)
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
+ val remoteDispatcher =
+ mock(RemoteDispatcher::class.java) as RemoteDispatcher<IWorkManagerImpl>
val callback = spy(RemoteCallback())
val session = SettableFuture.create<IWorkManagerImpl>()
session.setException(RuntimeException("Something bad happened"))
@@ -136,8 +155,13 @@
@Test
@MediumTest
public fun cleanUpOnSuccessfulDispatch() {
+ if (Build.VERSION.SDK_INT <= 27) {
+ // Exclude <= API 27, from tests because it causes a SIGSEGV.
+ return
+ }
+
val binder = mock(IBinder::class.java)
- val remoteDispatcher = RemoteWorkManagerClient.RemoteDispatcher { _, callback ->
+ val remoteDispatcher = RemoteDispatcher<IWorkManagerImpl> { _, callback ->
callback.onSuccess(ByteArray(0))
}
val remoteStub = mock(IWorkManagerImpl::class.java)
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
index f97bee9..f3e5022 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableCallback.java
@@ -81,16 +81,16 @@
public void run() {
try {
I result = mCallback.mFuture.get();
- successCallback(mCallback.mCallback, mCallback.toByteArray(result));
+ reportSuccess(mCallback.mCallback, mCallback.toByteArray(result));
} catch (Throwable throwable) {
- failureCallback(mCallback.mCallback, throwable);
+ reportFailure(mCallback.mCallback, throwable);
}
}
/**
* Dispatches successful callbacks safely.
*/
- public static void successCallback(
+ public static void reportSuccess(
@NonNull IWorkManagerImplCallback callback,
@NonNull byte[] response) {
try {
@@ -103,7 +103,7 @@
/**
* Dispatches failures callbacks safely.
**/
- public static void failureCallback(
+ public static void reportFailure(
@NonNull IWorkManagerImplCallback callback,
@NonNull Throwable throwable) {
try {
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
new file mode 100644
index 0000000..c4c1cca
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImpl.java
@@ -0,0 +1,204 @@
+/*
+ * 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.work.multiprocess;
+
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportSuccess;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Configuration;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import androidx.work.multiprocess.parcelable.ParcelConverters;
+import androidx.work.multiprocess.parcelable.ParcelableRemoteWorkRequest;
+import androidx.work.multiprocess.parcelable.ParcelableResult;
+import androidx.work.multiprocess.parcelable.ParcelableWorkerParameters;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An implementation of ListenableWorker that can be executed in a remote process.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ListenableWorkerImpl extends IListenableWorkerImpl.Stub {
+ // Synthetic access
+ static final String TAG = Logger.tagWithPrefix("ListenableWorkerImpl");
+ // Synthetic access
+ static byte[] sEMPTY = new byte[0];
+ // Synthetic access
+ static final Object sLock = new Object();
+
+ // Synthetic access
+ final Context mContext;
+ // Synthetic access
+ final WorkManagerImpl mWorkManager;
+ // Synthetic access
+ final Configuration mConfiguration;
+ // Synthetic access
+ final TaskExecutor mTaskExecutor;
+ // Synthetic access
+ final Map<String, ListenableFuture<ListenableWorker.Result>> mFutureMap;
+
+ ListenableWorkerImpl(@NonNull Context context) {
+ mContext = context.getApplicationContext();
+ mWorkManager = WorkManagerImpl.getInstance(mContext);
+ mConfiguration = mWorkManager.getConfiguration();
+ mTaskExecutor = mWorkManager.getWorkTaskExecutor();
+ mFutureMap = new HashMap<>();
+ }
+
+ @Override
+ public void startWork(
+ @NonNull final byte[] request,
+ @NonNull final IWorkManagerImplCallback callback) {
+ try {
+ ParcelableRemoteWorkRequest parcelableRemoteWorkRequest =
+ ParcelConverters.unmarshall(request, ParcelableRemoteWorkRequest.CREATOR);
+
+ ParcelableWorkerParameters parcelableWorkerParameters =
+ parcelableRemoteWorkRequest.getParcelableWorkerParameters();
+
+ WorkerParameters workerParameters =
+ parcelableWorkerParameters.toWorkerParameters(mWorkManager);
+
+ final String id = workerParameters.getId().toString();
+ final String workerClassName = parcelableRemoteWorkRequest.getWorkerClassName();
+
+ Logger.get().debug(TAG,
+ String.format("Executing work request (%s, %s)", id, workerClassName));
+
+ final ListenableFuture<ListenableWorker.Result> futureResult =
+ executeWorkRequest(id, workerClassName, workerParameters);
+
+ futureResult.addListener(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ListenableWorker.Result result = futureResult.get();
+ ParcelableResult parcelableResult = new ParcelableResult(result);
+ byte[] response = ParcelConverters.marshall(parcelableResult);
+ reportSuccess(callback, response);
+ } catch (ExecutionException | InterruptedException exception) {
+ reportFailure(callback, exception);
+ } catch (CancellationException cancellationException) {
+ Logger.get().debug(TAG, String.format("Worker (%s) was cancelled", id));
+ reportFailure(callback, cancellationException);
+ } finally {
+ synchronized (sLock) {
+ mFutureMap.remove(id);
+ }
+ }
+ }
+ }, mTaskExecutor.getBackgroundExecutor());
+ } catch (Throwable throwable) {
+ reportFailure(callback, throwable);
+ }
+ }
+
+ @Override
+ public void interrupt(
+ @NonNull final byte[] request,
+ @NonNull final IWorkManagerImplCallback callback) {
+ try {
+ ParcelableWorkerParameters parcelableWorkerParameters =
+ ParcelConverters.unmarshall(request, ParcelableWorkerParameters.CREATOR);
+ final String id = parcelableWorkerParameters.getId().toString();
+ Logger.get().debug(TAG, String.format("Interrupting work with id (%s)", id));
+
+ final ListenableFuture<ListenableWorker.Result> future;
+ synchronized (sLock) {
+ future = mFutureMap.remove(id);
+ }
+ if (future != null) {
+ mWorkManager.getWorkTaskExecutor().getBackgroundExecutor()
+ .execute(new Runnable() {
+ @Override
+ public void run() {
+ future.cancel(true);
+ reportSuccess(callback, sEMPTY);
+ }
+ });
+ } else {
+ // Nothing to do.
+ reportSuccess(callback, sEMPTY);
+ }
+ } catch (Throwable throwable) {
+ reportFailure(callback, throwable);
+ }
+ }
+
+ @NonNull
+ private ListenableFuture<ListenableWorker.Result> executeWorkRequest(
+ @NonNull String id,
+ @NonNull String workerClassName,
+ @NonNull WorkerParameters workerParameters) {
+
+ final SettableFuture<ListenableWorker.Result> future = SettableFuture.create();
+
+ Logger.get().debug(TAG,
+ String.format("Tracking execution of %s (%s)", id, workerClassName));
+
+ synchronized (sLock) {
+ mFutureMap.put(id, future);
+ }
+
+ ListenableWorker worker = mConfiguration.getWorkerFactory()
+ .createWorkerWithDefaultFallback(mContext, workerClassName, workerParameters);
+
+ if (worker == null) {
+ String message = String.format(
+ "Unable to create an instance of %s", workerClassName);
+ Logger.get().error(TAG, message);
+ future.setException(new IllegalStateException(message));
+ return future;
+ }
+
+ if (!(worker instanceof RemoteListenableWorker)) {
+ String message = String.format(
+ "%s does not extend %s",
+ workerClassName,
+ RemoteListenableWorker.class.getName()
+ );
+ Logger.get().error(TAG, message);
+ future.setException(new IllegalStateException(message));
+ return future;
+ }
+
+ try {
+ RemoteListenableWorker remoteListenableWorker = (RemoteListenableWorker) worker;
+ future.setFuture(remoteListenableWorker.startRemoteWork());
+ } catch (Throwable throwable) {
+ future.setException(throwable);
+ }
+
+ return future;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
new file mode 100644
index 0000000..5977450
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/ListenableWorkerImplClient.java
@@ -0,0 +1,190 @@
+/*
+ * 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.work.multiprocess;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+
+import android.annotation.SuppressLint;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Logger;
+import androidx.work.impl.utils.futures.SettableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/***
+ * A client for {@link IListenableWorkerImpl}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ListenableWorkerImplClient {
+ // Synthetic access
+ static final String TAG = Logger.tagWithPrefix("ListenableWorkerImplClient");
+
+ // Synthetic access
+ final Context mContext;
+
+ // Synthetic access
+ final Executor mExecutor;
+
+ public ListenableWorkerImplClient(
+ @NonNull Context context,
+ @NonNull Executor executor) {
+ mContext = context;
+ mExecutor = executor;
+ }
+
+ /**
+ * @return a {@link ListenableFuture} of {@link IListenableWorkerImpl} after a
+ * {@link ServiceConnection} is established.
+ */
+ @NonNull
+ public ListenableFuture<IListenableWorkerImpl> getListenableWorkerImpl(
+ @NonNull ComponentName component) {
+
+ Logger.get().debug(TAG,
+ String.format("Binding to %s, %s", component.getPackageName(),
+ component.getClassName()));
+
+ Connection session = new Connection();
+ try {
+ Intent intent = new Intent();
+ intent.setComponent(component);
+ boolean bound = mContext.bindService(intent, session, BIND_AUTO_CREATE);
+ if (!bound) {
+ unableToBind(session, new RuntimeException("Unable to bind to service"));
+ }
+ } catch (Throwable throwable) {
+ unableToBind(session, throwable);
+ }
+
+ return session.mFuture;
+ }
+
+ /**
+ * Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
+ * {@link RemoteDispatcher}.
+ */
+ @NonNull
+ public ListenableFuture<byte[]> execute(
+ @NonNull ComponentName componentName,
+ @NonNull RemoteDispatcher<IListenableWorkerImpl> dispatcher) {
+
+ ListenableFuture<IListenableWorkerImpl> session = getListenableWorkerImpl(componentName);
+ return execute(session, dispatcher, new RemoteCallback());
+ }
+
+ /**
+ * Executes a method on an instance of {@link IListenableWorkerImpl} using the instance of
+ * {@link RemoteDispatcher} and the {@link RemoteCallback}.
+ */
+ @NonNull
+ @SuppressLint("LambdaLast")
+ public ListenableFuture<byte[]> execute(
+ @NonNull ListenableFuture<IListenableWorkerImpl> session,
+ @NonNull final RemoteDispatcher<IListenableWorkerImpl> dispatcher,
+ @NonNull final RemoteCallback callback) {
+
+ session.addListener(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final IListenableWorkerImpl iListenableWorker = session.get();
+ callback.setBinder(iListenableWorker.asBinder());
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ dispatcher.execute(iListenableWorker, callback);
+ } catch (Throwable innerThrowable) {
+ Logger.get().error(TAG, "Unable to execute", innerThrowable);
+ reportFailure(callback, innerThrowable);
+ }
+ }
+ });
+ } catch (ExecutionException | InterruptedException exception) {
+ String message = "Unable to bind to service";
+ Logger.get().error(TAG, message, exception);
+ reportFailure(callback, exception);
+ }
+ }
+ }, mExecutor);
+ return callback.getFuture();
+ }
+
+ /**
+ * The implementation of {@link ServiceConnection} that handles changes in the connection.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static class Connection implements ServiceConnection {
+ private static final String TAG = Logger.tagWithPrefix("ListenableWorkerImplSession");
+
+ final SettableFuture<IListenableWorkerImpl> mFuture;
+
+ public Connection() {
+ mFuture = SettableFuture.create();
+ }
+
+ @Override
+ public void onServiceConnected(
+ @NonNull ComponentName componentName,
+ @NonNull IBinder iBinder) {
+ Logger.get().debug(TAG, "Service connected");
+ IListenableWorkerImpl iListenableWorkerImpl =
+ IListenableWorkerImpl.Stub.asInterface(iBinder);
+ mFuture.set(iListenableWorkerImpl);
+ }
+
+ @Override
+ public void onServiceDisconnected(@NonNull ComponentName componentName) {
+ Logger.get().warning(TAG, "Service disconnected");
+ mFuture.setException(new RuntimeException("Service disconnected"));
+ }
+
+ @Override
+ public void onBindingDied(@NonNull ComponentName name) {
+ Logger.get().warning(TAG, "Binding died");
+ mFuture.setException(new RuntimeException("Binding died"));
+ }
+
+ @Override
+ public void onNullBinding(@NonNull ComponentName name) {
+ Logger.get().error(TAG, "Unable to bind to service");
+ mFuture.setException(
+ new RuntimeException(String.format("Cannot bind to service %s", name)));
+ }
+ }
+
+ private static void unableToBind(@NonNull Connection session, @NonNull Throwable throwable) {
+ Logger.get().error(TAG, "Unable to bind to service", throwable);
+ session.mFuture.setException(throwable);
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java
new file mode 100644
index 0000000..e2da73e
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteClientUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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.work.multiprocess;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.arch.core.util.Function;
+import androidx.work.impl.utils.futures.SettableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A collection of utilities which make using
+ * {@link com.google.common.util.concurrent.ListenableFuture} easier.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class RemoteClientUtils {
+ private RemoteClientUtils() {
+ // Utilities
+ }
+
+ /**
+ * A mapper that essentially drops the byte[].
+ */
+ public static final Function<byte[], Void> sVoidMapper = new Function<byte[], Void>() {
+ @Override
+ public Void apply(byte[] input) {
+ return null;
+ }
+ };
+
+ /**
+ * Defines a mapper for a {@link ListenableFuture}.
+ */
+ @NonNull
+ public static <I, O> ListenableFuture<O> map(
+ @NonNull final ListenableFuture<I> input,
+ @NonNull final Function<I, O> transformation,
+ @NonNull Executor executor) {
+
+ final SettableFuture<O> output = SettableFuture.create();
+ input.addListener(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ I in = input.get();
+ O out = transformation.apply(in);
+ output.set(out);
+ } catch (Throwable throwable) {
+ Throwable cause = throwable.getCause();
+ cause = cause == null ? throwable : cause;
+ output.setException(cause);
+ }
+ }
+ }, executor);
+ return output;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
new file mode 100644
index 0000000..5a7a385
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteDispatcher.java
@@ -0,0 +1,40 @@
+/*
+ * 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.work.multiprocess;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * @param <T> The remote interface subtype that usually implements {@link android.os.IBinder}.
+ * @hide
+ */
+@SuppressLint("LambdaLast")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface RemoteDispatcher<T> {
+ /**
+ * Perform the actual work given an instance of {@link IWorkManagerImpl} and the
+ * {@link IWorkManagerImplCallback} callback.
+ *
+ * @param binder the remote interface implementation
+ * @param callback the {@link IWorkManagerImplCallback} instance
+ */
+ void execute(@NonNull T binder,
+ @NonNull IWorkManagerImplCallback callback) throws Throwable;
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
new file mode 100644
index 0000000..cb124f1
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteListenableWorker.java
@@ -0,0 +1,204 @@
+/*
+ * 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.work.multiprocess;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.RemoteException;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.arch.core.util.Function;
+import androidx.work.Data;
+import androidx.work.ListenableWorker;
+import androidx.work.Logger;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.model.WorkSpec;
+import androidx.work.impl.utils.futures.SettableFuture;
+import androidx.work.multiprocess.parcelable.ParcelConverters;
+import androidx.work.multiprocess.parcelable.ParcelableRemoteWorkRequest;
+import androidx.work.multiprocess.parcelable.ParcelableResult;
+import androidx.work.multiprocess.parcelable.ParcelableWorkerParameters;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.concurrent.Executor;
+
+
+/**
+ * Is an implementation of a {@link ListenableWorker} that can bind to a remote process.
+ * <p>
+ * To be able to bind to a remote process, A {@link RemoteListenableWorker} needs additional
+ * arguments as part of its input {@link Data}.
+ * <p>
+ * The arguments ({@link #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME}) are used to
+ * determine the {@link android.app.Service} that the {@link RemoteListenableWorker} can bind to.
+ * {@link #startRemoteWork()} is then subsequently called in the process that the
+ * {@link android.app.Service} is running in.
+ */
+public abstract class RemoteListenableWorker extends ListenableWorker {
+ // Synthetic access
+ static final String TAG = Logger.tagWithPrefix("RemoteListenableWorker");
+
+ /**
+ * The {@code #ARGUMENT_PACKAGE_NAME}, {@link #ARGUMENT_CLASS_NAME} together determine the
+ * {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
+ * {@link #startRemoteWork()}.
+ */
+ public static final String ARGUMENT_PACKAGE_NAME =
+ "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+
+ /**
+ * The {@link #ARGUMENT_PACKAGE_NAME}, {@code className} together determine the
+ * {@link ComponentName} that the {@link RemoteListenableWorker} binds to before calling
+ * {@link #startRemoteWork()}.
+ */
+ public static final String ARGUMENT_CLASS_NAME =
+ "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+
+ // Synthetic access
+ final WorkerParameters mWorkerParameters;
+
+ // Synthetic access
+ final WorkManagerImpl mWorkManager;
+
+ // Synthetic access
+ final Executor mExecutor;
+
+ // Synthetic access
+ final ListenableWorkerImplClient mClient;
+
+ // Synthetic access
+ @Nullable
+ String mWorkerClassName;
+
+ @Nullable
+ private ComponentName mComponentName;
+
+ /**
+ * @param appContext The application {@link Context}
+ * @param workerParams {@link WorkerParameters} to setup the internal state of this worker
+ */
+ public RemoteListenableWorker(
+ @NonNull Context appContext,
+ @NonNull WorkerParameters workerParams) {
+ super(appContext, workerParams);
+ mWorkerParameters = workerParams;
+ mWorkManager = WorkManagerImpl.getInstance(appContext);
+ mExecutor = mWorkManager.getWorkTaskExecutor().getBackgroundExecutor();
+ mClient = new ListenableWorkerImplClient(getApplicationContext(), mExecutor);
+ }
+
+ @Override
+ @NonNull
+ public final ListenableFuture<Result> startWork() {
+ SettableFuture<Result> future = SettableFuture.create();
+ Data data = getInputData();
+ final String id = mWorkerParameters.getId().toString();
+ String packageName = data.getString(ARGUMENT_PACKAGE_NAME);
+ String serviceClassName = data.getString(ARGUMENT_CLASS_NAME);
+
+ if (TextUtils.isEmpty(packageName)) {
+ String message = "Need to specify a package name for the Remote Service.";
+ Logger.get().error(TAG, message);
+ future.setException(new IllegalArgumentException(message));
+ return future;
+ }
+
+ if (TextUtils.isEmpty(serviceClassName)) {
+ String message = "Need to specify a class name for the Remote Service.";
+ Logger.get().error(TAG, message);
+ future.setException(new IllegalArgumentException(message));
+ return future;
+ }
+
+ mComponentName = new ComponentName(packageName, serviceClassName);
+
+ ListenableFuture<byte[]> result = mClient.execute(
+ mComponentName,
+ new RemoteDispatcher<IListenableWorkerImpl>() {
+ @Override
+ public void execute(
+ @NonNull IListenableWorkerImpl listenableWorkerImpl,
+ @NonNull IWorkManagerImplCallback callback) throws RemoteException {
+
+ WorkSpec workSpec = mWorkManager.getWorkDatabase()
+ .workSpecDao()
+ .getWorkSpec(id);
+
+ mWorkerClassName = workSpec.workerClassName;
+ ParcelableRemoteWorkRequest remoteWorkRequest =
+ new ParcelableRemoteWorkRequest(
+ workSpec.workerClassName, mWorkerParameters
+ );
+ byte[] request = ParcelConverters.marshall(remoteWorkRequest);
+ listenableWorkerImpl.startWork(request, callback);
+ }
+ });
+
+ return RemoteClientUtils.map(result, new Function<byte[], Result>() {
+ @Override
+ public Result apply(byte[] input) {
+ ParcelableResult parcelableResult = ParcelConverters.unmarshall(input,
+ ParcelableResult.CREATOR);
+ return parcelableResult.getResult();
+ }
+ }, mExecutor);
+ }
+
+ /**
+ * Override this method to define the work that needs to run in the remote process. This method
+ * is called on the main thread.
+ * <p>
+ * A ListenableWorker is given a maximum of ten minutes to finish its execution and return a
+ * {@code Result}. After this time has expired, the worker will be signalled to stop and its
+ * {@link ListenableFuture} will be cancelled. Note that the 10 minute execution window also
+ * includes the cost of binding to the remote process.
+ *
+ * @return A {@link ListenableFuture} with the {@code Result} of the computation. If you
+ * cancel this Future, WorkManager will treat this unit of work as a {@code Result#failure()}.
+ */
+ @NonNull
+ public abstract ListenableFuture<Result> startRemoteWork();
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void onStopped() {
+ super.onStopped();
+ // Delegate interruptions to the remote process.
+ if (mComponentName != null) {
+ mClient.execute(mComponentName,
+ new RemoteDispatcher<IListenableWorkerImpl>() {
+ @Override
+ public void execute(
+ @NonNull IListenableWorkerImpl listenableWorkerImpl,
+ @NonNull IWorkManagerImplCallback callback)
+ throws RemoteException {
+ ParcelableWorkerParameters parcelableWorkerParameters =
+ new ParcelableWorkerParameters(mWorkerParameters);
+ byte[] request = ParcelConverters.marshall(parcelableWorkerParameters);
+ listenableWorkerImpl.interrupt(request, callback);
+ }
+ });
+ }
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
index 59bb5b0f..039948a 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerClient.java
@@ -18,7 +18,9 @@
import static android.content.Context.BIND_AUTO_CREATE;
-import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.failureCallback;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
+import static androidx.work.multiprocess.RemoteClientUtils.map;
+import static androidx.work.multiprocess.RemoteClientUtils.sVoidMapper;
import android.annotation.SuppressLint;
import android.content.ComponentName;
@@ -77,6 +79,7 @@
private Session mSession;
+ @SuppressLint("BanKeepAnnotation")
@Keep
public RemoteWorkManagerClient(@NonNull Context context, @NonNull WorkManagerImpl workManager) {
mContext = context.getApplicationContext();
@@ -95,7 +98,7 @@
@NonNull
@Override
public ListenableFuture<Void> enqueue(@NonNull final List<WorkRequest> requests) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(
@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -150,7 +153,7 @@
@NonNull
@Override
public ListenableFuture<Void> enqueue(@NonNull final WorkContinuation continuation) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -166,7 +169,7 @@
@NonNull
@Override
public ListenableFuture<Void> cancelWorkById(@NonNull final UUID id) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -179,7 +182,7 @@
@NonNull
@Override
public ListenableFuture<Void> cancelAllWorkByTag(@NonNull final String tag) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -192,7 +195,7 @@
@NonNull
@Override
public ListenableFuture<Void> cancelUniqueWork(@NonNull final String uniqueWorkName) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -205,7 +208,7 @@
@NonNull
@Override
public ListenableFuture<Void> cancelAllWork() {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
@NonNull IWorkManagerImplCallback callback) throws Throwable {
@@ -218,7 +221,7 @@
@NonNull
@Override
public ListenableFuture<List<WorkInfo>> getWorkInfos(@NonNull final WorkQuery workQuery) {
- ListenableFuture<byte[]> result = execute(new RemoteDispatcher() {
+ ListenableFuture<byte[]> result = execute(new RemoteDispatcher<IWorkManagerImpl>() {
@Override
public void execute(
@NonNull IWorkManagerImpl iWorkManagerImpl,
@@ -244,7 +247,8 @@
* @return The {@link ListenableFuture} instance.
*/
@NonNull
- public ListenableFuture<byte[]> execute(@NonNull final RemoteDispatcher dispatcher) {
+ public ListenableFuture<byte[]> execute(
+ @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher) {
return execute(getSession(), dispatcher, new RemoteCallback());
}
@@ -261,7 +265,7 @@
@VisibleForTesting
ListenableFuture<byte[]> execute(
@NonNull final ListenableFuture<IWorkManagerImpl> session,
- @NonNull final RemoteDispatcher dispatcher,
+ @NonNull final RemoteDispatcher<IWorkManagerImpl> dispatcher,
@NonNull final RemoteCallback callback) {
session.addListener(new Runnable() {
@Override
@@ -277,13 +281,13 @@
dispatcher.execute(iWorkManager, callback);
} catch (Throwable innerThrowable) {
Logger.get().error(TAG, "Unable to execute", innerThrowable);
- failureCallback(callback, innerThrowable);
+ reportFailure(callback, innerThrowable);
}
}
});
} catch (ExecutionException | InterruptedException exception) {
Logger.get().error(TAG, "Unable to bind to service");
- failureCallback(callback, new RuntimeException("Unable to bind to service"));
+ reportFailure(callback, new RuntimeException("Unable to bind to service"));
cleanUp();
}
}
@@ -335,53 +339,6 @@
}
/**
- * A mapper that essentially drops the byte[].
- */
- private static final Function<byte[], Void> sVoidMapper = new Function<byte[], Void>() {
- @Override
- public Void apply(byte[] input) {
- return null;
- }
- };
-
- private static <I, O> ListenableFuture<O> map(
- @NonNull final ListenableFuture<I> input,
- @NonNull final Function<I, O> transformation,
- @NonNull Executor executor) {
-
- final SettableFuture<O> output = SettableFuture.create();
- input.addListener(new Runnable() {
- @Override
- public void run() {
- try {
- I in = input.get();
- O out = transformation.apply(in);
- output.set(out);
- } catch (Throwable throwable) {
- Throwable cause = throwable.getCause();
- cause = cause == null ? throwable : cause;
- output.setException(cause);
- }
- }
- }, executor);
- return output;
- }
-
- /**
- * @hide
- */
- @SuppressLint("LambdaLast")
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public interface RemoteDispatcher {
- /**
- * Perform the actual work given an instance of {@link IWorkManagerImpl} and the
- * {@link IWorkManagerImplCallback} callback.
- */
- void execute(@NonNull IWorkManagerImpl iWorkManagerImpl,
- @NonNull IWorkManagerImplCallback callback) throws Throwable;
- }
-
- /**
* The implementation of {@link ServiceConnection} that handles changes in the connection.
*
* @hide
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
index 134d478..68cb11d 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkManagerImpl.java
@@ -17,7 +17,7 @@
package androidx.work.multiprocess;
-import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.failureCallback;
+import static androidx.work.multiprocess.ListenableCallback.ListenableCallbackRunnable.reportFailure;
import android.content.Context;
@@ -81,7 +81,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -107,7 +107,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -127,7 +127,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -149,7 +149,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -171,7 +171,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -191,7 +191,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
@@ -214,7 +214,7 @@
};
listenableCallback.dispatchCallbackSafely();
} catch (Throwable throwable) {
- failureCallback(callback, throwable);
+ reportFailure(callback, throwable);
}
}
}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
new file mode 100644
index 0000000..4fbf6a6
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteWorkerService.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.multiprocess;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.work.Logger;
+
+/**
+ * The {@link Service} which hosts an implementation of a {@link androidx.work.ListenableWorker}.
+ */
+public class RemoteWorkerService extends Service {
+ static final String TAG = Logger.tagWithPrefix("RemoteWorkerService");
+ private IBinder mBinder;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mBinder = new ListenableWorkerImpl(this);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(@NonNull Intent intent) {
+ Logger.get().info(TAG, "Binding to RemoteWorkerService");
+ return mBinder;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java
new file mode 100644
index 0000000..46566ba
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableData.java
@@ -0,0 +1,249 @@
+/*
+ * 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.work.multiprocess.parcelable;
+
+import static androidx.work.Data.convertPrimitiveBooleanArray;
+import static androidx.work.Data.convertPrimitiveByteArray;
+import static androidx.work.Data.convertPrimitiveDoubleArray;
+import static androidx.work.Data.convertPrimitiveFloatArray;
+import static androidx.work.Data.convertPrimitiveIntArray;
+import static androidx.work.Data.convertPrimitiveLongArray;
+import static androidx.work.Data.convertToPrimitiveArray;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.work.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * {@link androidx.work.Data} but {@link android.os.Parcelable}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableData implements Parcelable {
+
+ // The list of supported types.
+ private static final byte TYPE_NULL = 0;
+ private static final byte TYPE_BOOLEAN = 1;
+ private static final byte TYPE_BYTE = 2;
+ private static final byte TYPE_INTEGER = 3;
+ private static final byte TYPE_LONG = 4;
+ private static final byte TYPE_FLOAT = 5;
+ private static final byte TYPE_DOUBLE = 6;
+ private static final byte TYPE_STRING = 7;
+ private static final byte TYPE_BOOLEAN_ARRAY = 8;
+ private static final byte TYPE_BYTE_ARRAY = 9;
+ private static final byte TYPE_INTEGER_ARRAY = 10;
+ private static final byte TYPE_LONG_ARRAY = 11;
+ private static final byte TYPE_FLOAT_ARRAY = 12;
+ private static final byte TYPE_DOUBLE_ARRAY = 13;
+ private static final byte TYPE_STRING_ARRAY = 14;
+
+ private final Data mData;
+
+ public ParcelableData(@NonNull Data data) {
+ mData = data;
+ }
+
+ protected ParcelableData(@NonNull Parcel in) {
+ Map<String, Object> values = new HashMap<>();
+ // size
+ int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ // entries
+ addEntry(in, values);
+ }
+ mData = new Data(values);
+ }
+
+ public static final Creator<ParcelableData> CREATOR =
+ new Creator<ParcelableData>() {
+ @Override
+ public ParcelableData createFromParcel(@NonNull Parcel in) {
+ return new ParcelableData(in);
+ }
+
+ @Override
+ public ParcelableData[] newArray(int size) {
+ return new ParcelableData[size];
+ }
+ };
+
+ @NonNull
+ public Data getData() {
+ return mData;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ Map<String, Object> keyValueMap = mData.getKeyValueMap();
+ // size
+ parcel.writeInt(keyValueMap.size());
+ for (Map.Entry<String, Object> entry : keyValueMap.entrySet()) {
+ writeToParcel(parcel, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private void writeToParcel(
+ @NonNull Parcel parcel,
+ @NonNull String key,
+ @Nullable Object value) {
+
+ // type + value
+ if (value == null) {
+ parcel.writeByte(TYPE_NULL);
+ } else {
+ Class<?> valueType = value.getClass();
+ if (valueType == Boolean.class) {
+ parcel.writeByte(TYPE_BOOLEAN);
+ boolean booleanValue = (Boolean) value;
+ writeBooleanValue(parcel, booleanValue);
+ } else if (valueType == Byte.class) {
+ parcel.writeByte(TYPE_BYTE);
+ byte byteValue = (Byte) value;
+ parcel.writeByte(byteValue);
+ } else if (valueType == Integer.class) {
+ parcel.writeByte(TYPE_INTEGER);
+ int intValue = (Integer) value;
+ parcel.writeInt(intValue);
+ } else if (valueType == Long.class) {
+ parcel.writeByte(TYPE_LONG);
+ Long longValue = (Long) value;
+ parcel.writeLong(longValue);
+ } else if (valueType == Float.class) {
+ parcel.writeByte(TYPE_FLOAT);
+ float floatValue = (Float) value;
+ parcel.writeFloat(floatValue);
+ } else if (valueType == Double.class) {
+ parcel.writeByte(TYPE_DOUBLE);
+ double doubleValue = (Double) value;
+ parcel.writeDouble(doubleValue);
+ } else if (valueType == String.class) {
+ parcel.writeByte(TYPE_STRING);
+ String stringValue = (String) value;
+ parcel.writeString(stringValue);
+ } else if (valueType == Boolean[].class) {
+ parcel.writeByte(TYPE_BOOLEAN_ARRAY);
+ Boolean[] booleanArray = (Boolean[]) value;
+ parcel.writeBooleanArray(convertToPrimitiveArray(booleanArray));
+ } else if (valueType == Byte[].class) {
+ parcel.writeByte(TYPE_BYTE_ARRAY);
+ Byte[] byteArray = (Byte[]) value;
+ parcel.writeByteArray(convertToPrimitiveArray(byteArray));
+ } else if (valueType == Integer[].class) {
+ parcel.writeByte(TYPE_INTEGER_ARRAY);
+ Integer[] integerArray = (Integer[]) value;
+ parcel.writeIntArray(convertToPrimitiveArray(integerArray));
+ } else if (valueType == Long[].class) {
+ parcel.writeByte(TYPE_LONG_ARRAY);
+ Long[] longArray = (Long[]) value;
+ parcel.writeLongArray(convertToPrimitiveArray(longArray));
+ } else if (valueType == Float[].class) {
+ parcel.writeByte(TYPE_FLOAT_ARRAY);
+ Float[] floatArray = (Float[]) value;
+ parcel.writeFloatArray(convertToPrimitiveArray(floatArray));
+ } else if (valueType == Double[].class) {
+ parcel.writeByte(TYPE_DOUBLE_ARRAY);
+ Double[] doubleArray = (Double[]) value;
+ parcel.writeDoubleArray(convertToPrimitiveArray(doubleArray));
+ } else if (valueType == String[].class) {
+ parcel.writeByte(TYPE_STRING_ARRAY);
+ String[] stringArray = (String[]) value;
+ parcel.writeStringArray(stringArray);
+ } else {
+ // Exhaustive check
+ String message = String.format("Unsupported value type %s", valueType.getName());
+ throw new IllegalArgumentException(message);
+ }
+ }
+ // key
+ parcel.writeString(key);
+ }
+
+ private void addEntry(@NonNull Parcel parcel, @NonNull Map<String, Object> values) {
+ // type
+ int type = parcel.readByte();
+ Object value = null;
+ switch (type) {
+ case TYPE_NULL:
+ break;
+ case TYPE_BYTE:
+ value = parcel.readByte();
+ break;
+ case TYPE_BOOLEAN:
+ value = readBooleanValue(parcel);
+ break;
+ case TYPE_INTEGER:
+ value = parcel.readInt();
+ break;
+ case TYPE_LONG:
+ value = parcel.readLong();
+ break;
+ case TYPE_FLOAT:
+ value = parcel.readFloat();
+ break;
+ case TYPE_DOUBLE:
+ value = parcel.readDouble();
+ break;
+ case TYPE_STRING:
+ value = parcel.readString();
+ break;
+ case TYPE_BOOLEAN_ARRAY:
+ value = convertPrimitiveBooleanArray(parcel.createBooleanArray());
+ break;
+ case TYPE_BYTE_ARRAY:
+ value = convertPrimitiveByteArray(parcel.createByteArray());
+ break;
+ case TYPE_INTEGER_ARRAY:
+ value = convertPrimitiveIntArray(parcel.createIntArray());
+ break;
+ case TYPE_LONG_ARRAY:
+ value = convertPrimitiveLongArray(parcel.createLongArray());
+ break;
+ case TYPE_FLOAT_ARRAY:
+ value = convertPrimitiveFloatArray(parcel.createFloatArray());
+ break;
+ case TYPE_DOUBLE_ARRAY:
+ value = convertPrimitiveDoubleArray(parcel.createDoubleArray());
+ break;
+ case TYPE_STRING_ARRAY:
+ value = parcel.createStringArray();
+ break;
+ default:
+ String message = String.format("Unsupported type %s", type);
+ throw new IllegalStateException(message);
+ }
+ String key = parcel.readString();
+ values.put(key, value);
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
new file mode 100644
index 0000000..d210690
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRemoteWorkRequest.java
@@ -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.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.WorkerParameters;
+
+/**
+ * Everything you need to run a {@link androidx.work.multiprocess.RemoteListenableWorker}.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableRemoteWorkRequest implements Parcelable {
+ // We are holding on to parcelables here instead of the actual deserialized representation.
+ // This is because, to create an instance of WorkerParameters we need the application context
+ // using which we can determine the configuration, taskExecutor to use etc.
+
+ private final String mWorkerClassName;
+ private final ParcelableWorkerParameters mParcelableWorkerParameters;
+
+ public ParcelableRemoteWorkRequest(
+ @NonNull String workerClassName,
+ @NonNull WorkerParameters workerParameters) {
+
+ mWorkerClassName = workerClassName;
+ mParcelableWorkerParameters = new ParcelableWorkerParameters(workerParameters);
+ }
+
+ protected ParcelableRemoteWorkRequest(@NonNull Parcel in) {
+ // workerClassName
+ mWorkerClassName = in.readString();
+ // parcelableWorkerParameters
+ mParcelableWorkerParameters = new ParcelableWorkerParameters(in);
+ }
+
+ public static final Creator<ParcelableRemoteWorkRequest> CREATOR =
+ new Creator<ParcelableRemoteWorkRequest>() {
+ @Override
+ public ParcelableRemoteWorkRequest createFromParcel(Parcel in) {
+ return new ParcelableRemoteWorkRequest(in);
+ }
+
+ @Override
+ public ParcelableRemoteWorkRequest[] newArray(int size) {
+ return new ParcelableRemoteWorkRequest[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ // workerClassName
+ parcel.writeString(mWorkerClassName);
+ // parcelableWorkerParameters
+ mParcelableWorkerParameters.writeToParcel(parcel, flags);
+ }
+
+ @NonNull
+ public String getWorkerClassName() {
+ return mWorkerClassName;
+ }
+
+ @NonNull
+ public ParcelableWorkerParameters getParcelableWorkerParameters() {
+ return mParcelableWorkerParameters;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
new file mode 100644
index 0000000..d24b96e
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableResult.java
@@ -0,0 +1,113 @@
+/*
+ * 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.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Data;
+import androidx.work.ListenableWorker;
+
+/**
+ * {@link androidx.work.ListenableWorker.Result}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableResult implements Parcelable {
+ private final ListenableWorker.Result mResult;
+
+ public ParcelableResult(@NonNull ListenableWorker.Result result) {
+ mResult = result;
+ }
+
+ public ParcelableResult(@NonNull Parcel in) {
+ // resultType
+ int resultType = in.readInt();
+ // outputData
+ ParcelableData parcelableOutputData = new ParcelableData(in);
+ mResult = intToResultType(resultType, parcelableOutputData.getData());
+ }
+
+ public static final Creator<ParcelableResult> CREATOR =
+ new Creator<ParcelableResult>() {
+ @Override
+ @NonNull
+ public ParcelableResult createFromParcel(Parcel in) {
+ return new ParcelableResult(in);
+ }
+
+ @Override
+ public ParcelableResult[] newArray(int size) {
+ return new ParcelableResult[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ int resultType = resultTypeOf(mResult);
+ // resultType
+ parcel.writeInt(resultType);
+ // outputData
+ Data outputData = mResult.getOutputData();
+ ParcelableData parcelableOutputData = new ParcelableData(outputData);
+ parcelableOutputData.writeToParcel(parcel, flags);
+ }
+
+ @NonNull
+ public ListenableWorker.Result getResult() {
+ return mResult;
+ }
+
+ private static int resultTypeOf(ListenableWorker.Result result) {
+ if (result instanceof ListenableWorker.Result.Retry) {
+ return 1;
+ } else if (result instanceof ListenableWorker.Result.Success) {
+ return 2;
+ } else if (result instanceof ListenableWorker.Result.Failure) {
+ return 3;
+ } else {
+ // Exhaustive check
+ throw new IllegalStateException(String.format("Unknown Result %s", result));
+ }
+ }
+
+ @NonNull
+ private static ListenableWorker.Result intToResultType(int resultType, @NonNull Data data) {
+ ListenableWorker.Result result = null;
+ if (resultType == 1) {
+ result = ListenableWorker.Result.retry();
+ } else if (resultType == 2) {
+ result = ListenableWorker.Result.success(data);
+ } else if (resultType == 3) {
+ result = ListenableWorker.Result.failure(data);
+ } else {
+ // Exhaustive check
+ throw new IllegalStateException(String.format("Unknown result type %s", resultType));
+ }
+ return result;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
new file mode 100644
index 0000000..f750cd2
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableRuntimeExtras.java
@@ -0,0 +1,150 @@
+/*
+ * 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.work.multiprocess.parcelable;
+
+import static androidx.work.multiprocess.parcelable.ParcelUtils.readBooleanValue;
+import static androidx.work.multiprocess.parcelable.ParcelUtils.writeBooleanValue;
+
+import android.annotation.SuppressLint;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.WorkerParameters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link androidx.work.WorkerParameters.RuntimeExtras}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableRuntimeExtras implements Parcelable {
+ private WorkerParameters.RuntimeExtras mRuntimeExtras;
+
+ public ParcelableRuntimeExtras(@NonNull WorkerParameters.RuntimeExtras runtimeExtras) {
+ mRuntimeExtras = runtimeExtras;
+ }
+
+ public ParcelableRuntimeExtras(@NonNull Parcel in) {
+ ClassLoader loader = getClass().getClassLoader();
+ // network
+ Network network = null;
+ boolean hasNetwork = readBooleanValue(in);
+ if (hasNetwork) {
+ network = in.readParcelable(loader);
+ }
+ // triggeredContentUris
+ List<Uri> triggeredContentUris = null;
+ boolean hasContentUris = readBooleanValue(in);
+ if (hasContentUris) {
+ Parcelable[] parceledUris = in.readParcelableArray(loader);
+ triggeredContentUris = new ArrayList<>(parceledUris.length);
+ for (Parcelable parcelable : parceledUris) {
+ triggeredContentUris.add((Uri) parcelable);
+ }
+ }
+ // triggeredContentAuthorities
+ List<String> triggeredContentAuthorities = null;
+ boolean hasContentAuthorities = readBooleanValue(in);
+ if (hasContentAuthorities) {
+ triggeredContentAuthorities = in.createStringArrayList();
+ }
+ mRuntimeExtras = new WorkerParameters.RuntimeExtras();
+ if (Build.VERSION.SDK_INT >= 28) {
+ mRuntimeExtras.network = network;
+ }
+ if (Build.VERSION.SDK_INT >= 24) {
+ if (triggeredContentUris != null) {
+ mRuntimeExtras.triggeredContentUris = triggeredContentUris;
+ }
+ if (triggeredContentAuthorities != null) {
+ mRuntimeExtras.triggeredContentAuthorities = triggeredContentAuthorities;
+ }
+ }
+ }
+
+ public static final Creator<ParcelableRuntimeExtras> CREATOR =
+ new Creator<ParcelableRuntimeExtras>() {
+ @Override
+ @NonNull
+ public ParcelableRuntimeExtras createFromParcel(Parcel in) {
+ return new ParcelableRuntimeExtras(in);
+ }
+
+ @Override
+ public ParcelableRuntimeExtras[] newArray(int size) {
+ return new ParcelableRuntimeExtras[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ @SuppressLint("NewApi")
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ Network network = null;
+ if (Build.VERSION.SDK_INT >= 28) {
+ network = mRuntimeExtras.network;
+ }
+ // network
+ boolean hasNetwork = network != null;
+ writeBooleanValue(parcel, hasNetwork);
+ if (hasNetwork) {
+ parcel.writeParcelable(network, flags);
+ }
+
+ List<Uri> triggeredContentUris = null;
+ List<String> triggeredAuthorities = null;
+ if (Build.VERSION.SDK_INT >= 24) {
+ triggeredContentUris = mRuntimeExtras.triggeredContentUris;
+ triggeredAuthorities = mRuntimeExtras.triggeredContentAuthorities;
+ }
+ // triggeredContentUris
+ boolean hasContentUris = triggeredContentUris != null && !triggeredContentUris.isEmpty();
+ writeBooleanValue(parcel, hasContentUris);
+ if (hasContentUris) {
+ Uri[] contentUriArray = new Uri[triggeredContentUris.size()];
+ for (int i = 0; i < contentUriArray.length; i++) {
+ contentUriArray[i] = triggeredContentUris.get(i);
+ }
+ parcel.writeParcelableArray(contentUriArray, flags);
+ }
+ // triggeredContentAuthorities
+ boolean hasContentAuthorities =
+ triggeredAuthorities != null && !triggeredAuthorities.isEmpty();
+ writeBooleanValue(parcel, hasContentAuthorities);
+ if (hasContentAuthorities) {
+ parcel.writeStringList(triggeredAuthorities);
+ }
+ }
+
+ @NonNull
+ public WorkerParameters.RuntimeExtras getRuntimeExtras() {
+ return mRuntimeExtras;
+ }
+}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
index ee15125..a37f194 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkInfo.java
@@ -56,12 +56,14 @@
// state
WorkInfo.State state = intToState(parcel.readInt());
// outputData
- Data output = Data.fromByteArray(parcel.createByteArray());
+ ParcelableData parcelableOutputData = new ParcelableData(parcel);
+ Data output = parcelableOutputData.getData();
// tags
String[] tagsArray = parcel.createStringArray();
List<String> tags = Arrays.asList(tagsArray);
// progress
- Data progress = Data.fromByteArray(parcel.createByteArray());
+ ParcelableData parcelableProgressData = new ParcelableData(parcel);
+ Data progress = parcelableProgressData.getData();
// runAttemptCount
int runAttemptCount = parcel.readInt();
mWorkInfo = new WorkInfo(id, state, output, tags, progress, runAttemptCount);
@@ -97,15 +99,15 @@
// state
parcel.writeInt(stateToInt(mWorkInfo.getState()));
// outputData
- byte[] outputData = mWorkInfo.getOutputData().toByteArray();
- parcel.writeByteArray(outputData);
+ ParcelableData parcelableOutputData = new ParcelableData(mWorkInfo.getOutputData());
+ parcelableOutputData.writeToParcel(parcel, flags);
// tags
// Note: converting to a String[] because that is faster.
List<String> tags = new ArrayList<>(mWorkInfo.getTags());
parcel.writeStringArray(tags.toArray(sEMPTY));
// progress
- byte[] progress = mWorkInfo.getProgress().toByteArray();
- parcel.writeByteArray(progress);
+ ParcelableData parcelableProgress = new ParcelableData(mWorkInfo.getProgress());
+ parcelableProgress.writeToParcel(parcel, flags);
// runAttemptCount
parcel.writeInt(mWorkInfo.getRunAttemptCount());
}
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
index dd0da14..e784b12 100644
--- a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkRequest.java
@@ -27,7 +27,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
-import androidx.work.Data;
import androidx.work.WorkRequest;
import androidx.work.impl.WorkRequestHolder;
import androidx.work.impl.model.WorkSpec;
@@ -65,9 +64,11 @@
// state
workSpec.state = intToState(in.readInt());
// inputData
- workSpec.input = Data.fromByteArray(in.createByteArray());
+ ParcelableData parcelableInputData = new ParcelableData(in);
+ workSpec.input = parcelableInputData.getData();
// outputData
- workSpec.output = Data.fromByteArray(in.createByteArray());
+ ParcelableData parcelableOutputData = new ParcelableData(in);
+ workSpec.output = parcelableOutputData.getData();
// initialDelay
workSpec.initialDelay = in.readLong();
// intervalDuration
@@ -125,11 +126,11 @@
// state
parcel.writeInt(stateToInt(workSpec.state));
// inputData
- byte[] inputData = workSpec.input.toByteArray();
- parcel.writeByteArray(inputData);
+ ParcelableData parcelableInputData = new ParcelableData(workSpec.input);
+ parcelableInputData.writeToParcel(parcel, flags);
// outputData
- byte[] outputData = workSpec.output.toByteArray();
- parcel.writeByteArray(outputData);
+ ParcelableData parcelableOutputData = new ParcelableData(workSpec.output);
+ parcelableOutputData.writeToParcel(parcel, flags);
// initialDelay
parcel.writeLong(workSpec.initialDelay);
// intervalDuration
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
new file mode 100644
index 0000000..9b8f0f5
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
@@ -0,0 +1,162 @@
+/*
+ * 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.work.multiprocess.parcelable;
+
+import android.annotation.SuppressLint;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.work.Configuration;
+import androidx.work.Data;
+import androidx.work.WorkerParameters;
+import androidx.work.impl.WorkDatabase;
+import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.foreground.ForegroundProcessor;
+import androidx.work.impl.utils.WorkForegroundUpdater;
+import androidx.work.impl.utils.WorkProgressUpdater;
+import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * {@link androidx.work.WorkerParameters}, but parcelable.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@SuppressLint("BanParcelableUsage")
+public class ParcelableWorkerParameters implements Parcelable {
+ @NonNull
+ private final UUID mId;
+ @NonNull
+ private final Data mData;
+ @NonNull
+ private final Set<String> mTags;
+ @NonNull
+ private final WorkerParameters.RuntimeExtras mRuntimeExtras;
+ private final int mRunAttemptCount;
+
+ public ParcelableWorkerParameters(@NonNull WorkerParameters parameters) {
+ mId = parameters.getId();
+ mData = parameters.getInputData();
+ mTags = parameters.getTags();
+ mRuntimeExtras = parameters.getRuntimeExtras();
+ mRunAttemptCount = parameters.getRunAttemptCount();
+ }
+
+ public static final Creator<ParcelableWorkerParameters> CREATOR =
+ new Creator<ParcelableWorkerParameters>() {
+ @Override
+ @NonNull
+ public ParcelableWorkerParameters createFromParcel(Parcel in) {
+ return new ParcelableWorkerParameters(in);
+ }
+
+ @Override
+ public ParcelableWorkerParameters[] newArray(int size) {
+ return new ParcelableWorkerParameters[size];
+ }
+ };
+
+ public ParcelableWorkerParameters(@NonNull Parcel in) {
+ // id
+ String id = in.readString();
+ mId = UUID.fromString(id);
+ // inputData
+ ParcelableData parcelableInputData = new ParcelableData(in);
+ mData = parcelableInputData.getData();
+ // tags
+ mTags = new HashSet<>(in.createStringArrayList());
+ // runtimeExtras
+ ParcelableRuntimeExtras parcelableRuntimeExtras = new ParcelableRuntimeExtras(in);
+ mRuntimeExtras = parcelableRuntimeExtras.getRuntimeExtras();
+ // runAttemptCount
+ mRunAttemptCount = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel parcel, int flags) {
+ // id
+ parcel.writeString(mId.toString());
+ // inputData
+ ParcelableData parcelableInputData = new ParcelableData(mData);
+ parcelableInputData.writeToParcel(parcel, flags);
+ // tags
+ List<String> tags = new ArrayList<>(mTags);
+ parcel.writeStringList(tags);
+ // runtimeExtras
+ ParcelableRuntimeExtras parcelableRuntimeExtras =
+ new ParcelableRuntimeExtras(mRuntimeExtras);
+ parcelableRuntimeExtras.writeToParcel(parcel, flags);
+ // runAttemptCount
+ parcel.writeInt(mRunAttemptCount);
+ }
+
+ @NonNull
+ public UUID getId() {
+ return mId;
+ }
+
+ @NonNull
+ public Data getData() {
+ return mData;
+ }
+
+ public int getRunAttemptCount() {
+ return mRunAttemptCount;
+ }
+
+ @NonNull
+ public Set<String> getTags() {
+ return mTags;
+ }
+
+ /**
+ * Converts {@link ParcelableWorkerParameters} to an instance of {@link WorkerParameters}
+ * lazily.
+ */
+ @NonNull
+ public WorkerParameters toWorkerParameters(@NonNull WorkManagerImpl workManager) {
+ Configuration configuration = workManager.getConfiguration();
+ WorkDatabase workDatabase = workManager.getWorkDatabase();
+ TaskExecutor taskExecutor = workManager.getWorkTaskExecutor();
+ ForegroundProcessor foregroundProcessor = workManager.getProcessor();
+ return new WorkerParameters(
+ mId,
+ mData,
+ mTags,
+ mRuntimeExtras,
+ mRunAttemptCount,
+ configuration.getExecutor(),
+ taskExecutor,
+ configuration.getWorkerFactory(),
+ new WorkProgressUpdater(workDatabase, taskExecutor),
+ new WorkForegroundUpdater(workDatabase, foregroundProcessor, taskExecutor)
+ );
+ }
+}
diff --git a/work/workmanager/api/current.txt b/work/workmanager/api/current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/current.txt
+++ b/work/workmanager/api/current.txt
@@ -182,6 +182,7 @@
enum_constant public static final androidx.work.NetworkType METERED;
enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+ enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
enum_constant public static final androidx.work.NetworkType UNMETERED;
}
@@ -300,6 +301,12 @@
method public abstract androidx.work.Operation pruneWork();
}
+ public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+ ctor public WorkManagerInitializer();
+ method public androidx.work.WorkManager create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
public final class WorkQuery {
method public java.util.List<java.util.UUID!> getIds();
method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/api/public_plus_experimental_current.txt b/work/workmanager/api/public_plus_experimental_current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/public_plus_experimental_current.txt
+++ b/work/workmanager/api/public_plus_experimental_current.txt
@@ -182,6 +182,7 @@
enum_constant public static final androidx.work.NetworkType METERED;
enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+ enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
enum_constant public static final androidx.work.NetworkType UNMETERED;
}
@@ -300,6 +301,12 @@
method public abstract androidx.work.Operation pruneWork();
}
+ public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+ ctor public WorkManagerInitializer();
+ method public androidx.work.WorkManager create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
public final class WorkQuery {
method public java.util.List<java.util.UUID!> getIds();
method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/api/restricted_current.txt b/work/workmanager/api/restricted_current.txt
index 2a4986c..1151de1 100644
--- a/work/workmanager/api/restricted_current.txt
+++ b/work/workmanager/api/restricted_current.txt
@@ -182,6 +182,7 @@
enum_constant public static final androidx.work.NetworkType METERED;
enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+ enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
enum_constant public static final androidx.work.NetworkType UNMETERED;
}
@@ -300,6 +301,12 @@
method public abstract androidx.work.Operation pruneWork();
}
+ public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+ ctor public WorkManagerInitializer();
+ method public androidx.work.WorkManager create(android.content.Context);
+ method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+ }
+
public final class WorkQuery {
method public java.util.List<java.util.UUID!> getIds();
method public java.util.List<androidx.work.WorkInfo.State!> getStates();
diff --git a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index a36e628..32786ae 100644
--- a/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/workmanager/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -16,6 +16,8 @@
package androidx.work.impl.background.systemjob;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED;
+
import static androidx.work.NetworkType.CONNECTED;
import static androidx.work.NetworkType.METERED;
import static androidx.work.NetworkType.NOT_REQUIRED;
@@ -26,8 +28,10 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.junit.Assert.assertTrue;
import android.app.job.JobInfo;
+import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Build;
@@ -303,6 +307,18 @@
is(JobInfo.NETWORK_TYPE_METERED));
}
+ @Test
+ @SmallTest
+ @SdkSuppress(minSdkVersion = 30)
+ public void testConvertNetworkType_temporarilyMetered() {
+ WorkSpec workSpec = getTestWorkSpecWithConstraints(new Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
+ .build());
+ JobInfo jobInfo = mConverter.convert(workSpec, JOB_ID);
+ NetworkRequest networkRequest = jobInfo.getRequiredNetwork();
+ assertTrue(networkRequest.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED));
+ }
+
private WorkSpec getTestWorkSpecWithConstraints(Constraints constraints) {
return getWorkSpec(new OneTimeWorkRequest.Builder(TestWorker.class)
.setConstraints(constraints)
diff --git a/work/workmanager/src/main/AndroidManifest.xml b/work/workmanager/src/main/AndroidManifest.xml
index 08d71dc..c772c5e 100644
--- a/work/workmanager/src/main/AndroidManifest.xml
+++ b/work/workmanager/src/main/AndroidManifest.xml
@@ -29,7 +29,7 @@
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
- <meta-data android:name="androidx.work.impl.WorkManagerInitializer"
+ <meta-data android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup" />
</provider>
<service
diff --git a/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl b/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
new file mode 100644
index 0000000..9ff1a3a
--- /dev/null
+++ b/work/workmanager/src/main/aidl/androidx/work/multiprocess/IListenableWorkerImpl.aidl
@@ -0,0 +1,35 @@
+/*
+ * 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.work.multiprocess;
+
+import androidx.work.multiprocess.IWorkManagerImplCallback;
+
+/**
+ * Implementation for a multi-process {@link ListenableWorker}.
+ *
+ * @hide
+ */
+oneway interface IListenableWorkerImpl {
+ // request is a ParcelablelRemoteRequest instance.
+ // callback gets a parcelized representation of Result
+ oneway void startWork(in byte[] request, IWorkManagerImplCallback callback);
+
+ // interrupt request.
+ // request is a ParcelableWorkerParameters instance.
+ // callback gets an empty result
+ oneway void interrupt(in byte[] request, IWorkManagerImplCallback callback);
+}
diff --git a/work/workmanager/src/main/java/androidx/work/Configuration.java b/work/workmanager/src/main/java/androidx/work/Configuration.java
index 6ddf8d2..c69547c 100644
--- a/work/workmanager/src/main/java/androidx/work/Configuration.java
+++ b/work/workmanager/src/main/java/androidx/work/Configuration.java
@@ -497,7 +497,7 @@
* A class that can provide the {@link Configuration} for WorkManager and allow for on-demand
* initialization of WorkManager. To do this:
* <p><ul>
- * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest</li>
+ * <li>Disable {@code androidx.work.WorkManagerInitializer} in your manifest</li>
* <li>Implement the {@link Configuration.Provider} interface on your
* {@link android.app.Application} class</li>
* <li>Use {@link WorkManager#getInstance(Context)} when accessing WorkManger (NOT
diff --git a/work/workmanager/src/main/java/androidx/work/Data.java b/work/workmanager/src/main/java/androidx/work/Data.java
index 156240e..578e83e 100644
--- a/work/workmanager/src/main/java/androidx/work/Data.java
+++ b/work/workmanager/src/main/java/androidx/work/Data.java
@@ -64,7 +64,7 @@
public static final int MAX_DATA_BYTES = 10 * 1024; // 10KB
@SuppressWarnings("WeakerAccess") /* synthetic access */
- Map<String, Object> mValues;
+ Map<String, Object> mValues;
Data() { // stub required for room
}
@@ -73,14 +73,18 @@
mValues = new HashMap<>(other.mValues);
}
- Data(@NonNull Map<String, ?> values) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Data(@NonNull Map<String, ?> values) {
mValues = new HashMap<>(values);
}
/**
* Gets the boolean value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -99,15 +103,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable boolean[] getBooleanArray(@NonNull String key) {
+ @Nullable
+ public boolean[] getBooleanArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Boolean[]) {
Boolean[] array = (Boolean[]) value;
- boolean[] returnArray = new boolean[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -116,7 +117,7 @@
/**
* Gets the byte value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -135,15 +136,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable byte[] getByteArray(@NonNull String key) {
+ @Nullable
+ public byte[] getByteArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Byte[]) {
Byte[] array = (Byte[]) value;
- byte[] returnArray = new byte[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -152,7 +150,7 @@
/**
* Gets the integer value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -171,15 +169,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable int[] getIntArray(@NonNull String key) {
+ @Nullable
+ public int[] getIntArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Integer[]) {
Integer[] array = (Integer[]) value;
- int[] returnArray = new int[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -188,7 +183,7 @@
/**
* Gets the long value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -207,15 +202,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable long[] getLongArray(@NonNull String key) {
+ @Nullable
+ public long[] getLongArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Long[]) {
Long[] array = (Long[]) value;
- long[] returnArray = new long[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -224,7 +216,7 @@
/**
* Gets the float value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -243,15 +235,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable float[] getFloatArray(@NonNull String key) {
+ @Nullable
+ public float[] getFloatArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Float[]) {
Float[] array = (Float[]) value;
- float[] returnArray = new float[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -260,7 +249,7 @@
/**
* Gets the double value for the given key.
*
- * @param key The key for the argument
+ * @param key The key for the argument
* @param defaultValue The default value to return if the key is not found
* @return The value specified by the key if it exists; the default value otherwise
*/
@@ -279,15 +268,12 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable double[] getDoubleArray(@NonNull String key) {
+ @Nullable
+ public double[] getDoubleArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof Double[]) {
Double[] array = (Double[]) value;
- double[] returnArray = new double[array.length];
- for (int i = 0; i < array.length; ++i) {
- returnArray[i] = array[i];
- }
- return returnArray;
+ return convertToPrimitiveArray(array);
} else {
return null;
}
@@ -299,7 +285,8 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; the default value otherwise
*/
- public @Nullable String getString(@NonNull String key) {
+ @Nullable
+ public String getString(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String) {
return (String) value;
@@ -314,7 +301,8 @@
* @param key The key for the argument
* @return The value specified by the key if it exists; {@code null} otherwise
*/
- public @Nullable String[] getStringArray(@NonNull String key) {
+ @Nullable
+ public String[] getStringArray(@NonNull String key) {
Object value = mValues.get(key);
if (value instanceof String[]) {
return (String[]) value;
@@ -329,7 +317,8 @@
* @return A {@link Map} of key-value pairs for this object; this Map is unmodifiable and should
* be used for reads only.
*/
- public @NonNull Map<String, Object> getKeyValueMap() {
+ @NonNull
+ public Map<String, Object> getKeyValueMap() {
return Collections.unmodifiableMap(mValues);
}
@@ -347,7 +336,7 @@
return Data.toByteArrayInternal(this);
}
- /**
+ /**
* Returns {@code true} if the instance of {@link Data} has a non-null value corresponding to
* the given {@link String} key with the expected type of {@code T}.
*
@@ -383,7 +372,8 @@
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@TypeConverter
- public static @NonNull byte[] toByteArrayInternal(@NonNull Data data) {
+ @NonNull
+ public static byte[] toByteArrayInternal(@NonNull Data data) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = null;
try {
@@ -429,7 +419,8 @@
* @throws IllegalStateException if bytes is bigger than {@link #MAX_DATA_BYTES}
*/
@TypeConverter
- public static @NonNull Data fromByteArray(@NonNull byte[] bytes) {
+ @NonNull
+ public static Data fromByteArray(@NonNull byte[] bytes) {
if (bytes.length > MAX_DATA_BYTES) {
throw new IllegalStateException(
"Data cannot occupy more than " + MAX_DATA_BYTES + " bytes when serialized");
@@ -521,8 +512,12 @@
return sb.toString();
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
+ /**
+ * * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Boolean[] convertPrimitiveBooleanArray(@NonNull boolean[] value) {
Boolean[] returnValue = new Boolean[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -530,8 +525,12 @@
return returnValue;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Byte[] convertPrimitiveByteArray(@NonNull byte[] value) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Byte[] convertPrimitiveByteArray(@NonNull byte[] value) {
Byte[] returnValue = new Byte[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -539,8 +538,12 @@
return returnValue;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Integer[] convertPrimitiveIntArray(@NonNull int[] value) {
Integer[] returnValue = new Integer[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -548,8 +551,12 @@
return returnValue;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Long[] convertPrimitiveLongArray(@NonNull long[] value) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Long[] convertPrimitiveLongArray(@NonNull long[] value) {
Long[] returnValue = new Long[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -557,8 +564,12 @@
return returnValue;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Float[] convertPrimitiveFloatArray(@NonNull float[] value) {
Float[] returnValue = new Float[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -566,8 +577,12 @@
return returnValue;
}
- @SuppressWarnings("WeakerAccess") /* synthetic access */
- static @NonNull Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static Double[] convertPrimitiveDoubleArray(@NonNull double[] value) {
Double[] returnValue = new Double[value.length];
for (int i = 0; i < value.length; ++i) {
returnValue[i] = value[i];
@@ -576,6 +591,84 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static boolean[] convertToPrimitiveArray(@NonNull Boolean[] array) {
+ boolean[] returnArray = new boolean[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static byte[] convertToPrimitiveArray(@NonNull Byte[] array) {
+ byte[] returnArray = new byte[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static int[] convertToPrimitiveArray(@NonNull Integer[] array) {
+ int[] returnArray = new int[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static long[] convertToPrimitiveArray(@NonNull Long[] array) {
+ long[] returnArray = new long[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static float[] convertToPrimitiveArray(@NonNull Float[] array) {
+ float[] returnArray = new float[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public static double[] convertToPrimitiveArray(@NonNull Double[] array) {
+ double[] returnArray = new double[array.length];
+ for (int i = 0; i < array.length; ++i) {
+ returnArray[i] = array[i];
+ }
+ return returnArray;
+ }
+
+ /**
* A builder for {@link Data} objects.
*/
public static final class Builder {
@@ -585,11 +678,12 @@
/**
* Puts a boolean into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putBoolean(@NonNull String key, boolean value) {
+ @NonNull
+ public Builder putBoolean(@NonNull String key, boolean value) {
mValues.put(key, value);
return this;
}
@@ -597,11 +691,12 @@
/**
* Puts a boolean array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
+ @NonNull
+ public Builder putBooleanArray(@NonNull String key, @NonNull boolean[] value) {
mValues.put(key, convertPrimitiveBooleanArray(value));
return this;
}
@@ -609,11 +704,12 @@
/**
* Puts an byte into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putByte(@NonNull String key, byte value) {
+ @NonNull
+ public Builder putByte(@NonNull String key, byte value) {
mValues.put(key, value);
return this;
}
@@ -621,11 +717,12 @@
/**
* Puts an integer array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
+ @NonNull
+ public Builder putByteArray(@NonNull String key, @NonNull byte[] value) {
mValues.put(key, convertPrimitiveByteArray(value));
return this;
}
@@ -633,11 +730,12 @@
/**
* Puts an integer into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putInt(@NonNull String key, int value) {
+ @NonNull
+ public Builder putInt(@NonNull String key, int value) {
mValues.put(key, value);
return this;
}
@@ -645,11 +743,12 @@
/**
* Puts an integer array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putIntArray(@NonNull String key, @NonNull int[] value) {
+ @NonNull
+ public Builder putIntArray(@NonNull String key, @NonNull int[] value) {
mValues.put(key, convertPrimitiveIntArray(value));
return this;
}
@@ -657,11 +756,12 @@
/**
* Puts a long into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putLong(@NonNull String key, long value) {
+ @NonNull
+ public Builder putLong(@NonNull String key, long value) {
mValues.put(key, value);
return this;
}
@@ -669,11 +769,12 @@
/**
* Puts a long array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putLongArray(@NonNull String key, @NonNull long[] value) {
+ @NonNull
+ public Builder putLongArray(@NonNull String key, @NonNull long[] value) {
mValues.put(key, convertPrimitiveLongArray(value));
return this;
}
@@ -681,11 +782,12 @@
/**
* Puts a float into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putFloat(@NonNull String key, float value) {
+ @NonNull
+ public Builder putFloat(@NonNull String key, float value) {
mValues.put(key, value);
return this;
}
@@ -693,11 +795,12 @@
/**
* Puts a float array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
+ @NonNull
+ public Builder putFloatArray(@NonNull String key, @NonNull float[] value) {
mValues.put(key, convertPrimitiveFloatArray(value));
return this;
}
@@ -705,11 +808,12 @@
/**
* Puts a double into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putDouble(@NonNull String key, double value) {
+ @NonNull
+ public Builder putDouble(@NonNull String key, double value) {
mValues.put(key, value);
return this;
}
@@ -717,11 +821,12 @@
/**
* Puts a double array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
+ @NonNull
+ public Builder putDoubleArray(@NonNull String key, @NonNull double[] value) {
mValues.put(key, convertPrimitiveDoubleArray(value));
return this;
}
@@ -729,11 +834,12 @@
/**
* Puts a String into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putString(@NonNull String key, @Nullable String value) {
+ @NonNull
+ public Builder putString(@NonNull String key, @Nullable String value) {
mValues.put(key, value);
return this;
}
@@ -741,11 +847,12 @@
/**
* Puts a String array into the arguments.
*
- * @param key The key for this argument
+ * @param key The key for this argument
* @param value The value for this argument
* @return The {@link Builder}
*/
- public @NonNull Builder putStringArray(@NonNull String key, @NonNull String[] value) {
+ @NonNull
+ public Builder putStringArray(@NonNull String key, @NonNull String[] value) {
mValues.put(key, value);
return this;
}
@@ -759,7 +866,8 @@
* @param data {@link Data} containing key-value pairs to add
* @return The {@link Builder}
*/
- public @NonNull Builder putAll(@NonNull Data data) {
+ @NonNull
+ public Builder putAll(@NonNull Data data) {
putAll(data.mValues);
return this;
}
@@ -773,7 +881,8 @@
* @param values A {@link Map} of key-value pairs to add
* @return The {@link Builder}
*/
- public @NonNull Builder putAll(@NonNull Map<String, Object> values) {
+ @NonNull
+ public Builder putAll(@NonNull Map<String, Object> values) {
for (Map.Entry<String, Object> entry : values.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
@@ -787,13 +896,14 @@
* Long, Float, Double, String, and array versions of each of those types.
* Invalid types throw an {@link IllegalArgumentException}.
*
- * @param key A {@link String} key to add
+ * @param key A {@link String} key to add
* @param value A nullable {@link Object} value to add of the valid types
* @return The {@link Builder}
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public @NonNull Builder put(@NonNull String key, @Nullable Object value) {
+ @NonNull
+ public Builder put(@NonNull String key, @Nullable Object value) {
if (value == null) {
mValues.put(key, null);
} else {
@@ -837,9 +947,10 @@
* Builds a {@link Data} object.
*
* @return The {@link Data} object containing all key-value pairs specified by this
- * {@link Builder}.
+ * {@link Builder}.
*/
- public @NonNull Data build() {
+ @NonNull
+ public Data build() {
Data data = new Data(mValues);
// Make sure we catch Data objects that are too large at build() instead of later. This
// method will throw an exception if data is too big.
diff --git a/work/workmanager/src/main/java/androidx/work/NetworkType.java b/work/workmanager/src/main/java/androidx/work/NetworkType.java
index 20f6d69..06d7e3a 100644
--- a/work/workmanager/src/main/java/androidx/work/NetworkType.java
+++ b/work/workmanager/src/main/java/androidx/work/NetworkType.java
@@ -16,6 +16,8 @@
package androidx.work;
+import androidx.annotation.RequiresApi;
+
/**
* An enumeration of various network types that can be used as {@link Constraints} for work.
*/
@@ -45,5 +47,16 @@
/**
* A metered network connection is required for this work.
*/
- METERED
+ METERED,
+
+ /**
+ * A temporarily unmetered Network. This capability will be set for networks that are
+ * generally metered, but are currently unmetered.
+ *
+ * Note: This capability can be changed at any time. When it is removed,
+ * {@link ListenableWorker}s are responsible for stopping any data transfer that should not
+ * occur on a metered network.
+ */
+ @RequiresApi(30)
+ TEMPORARILY_UNMETERED
}
diff --git a/work/workmanager/src/main/java/androidx/work/WorkManager.java b/work/workmanager/src/main/java/androidx/work/WorkManager.java
index 29a7982..4ae19f2 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkManager.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManager.java
@@ -189,7 +189,7 @@
* {@link Configuration}. By default, this method should not be called because WorkManager is
* automatically initialized. To initialize WorkManager yourself, please follow these steps:
* <p><ul>
- * <li>Disable {@code androidx.work.impl.WorkManagerInitializer} in your manifest.
+ * <li>Disable {@code androidx.work.WorkManagerInitializer} in your manifest.
* <li>Invoke this method in {@code Application#onCreate} or a {@code ContentProvider}. Note
* that this method <b>must</b> be invoked in one of these two places or you risk getting a
* {@code NullPointerException} in {@link #getInstance(Context)}.
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java b/work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
similarity index 83%
rename from work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java
rename to work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
index 39f1b1c3..93acbdf 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkManagerInitializer.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkManagerInitializer.java
@@ -14,27 +14,20 @@
* limitations under the License.
*/
-package androidx.work.impl;
+package androidx.work;
import android.content.Context;
import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
import androidx.startup.Initializer;
-import androidx.work.Configuration;
-import androidx.work.Logger;
-import androidx.work.WorkManager;
import java.util.Collections;
import java.util.List;
/**
* Initializes {@link androidx.work.WorkManager} using {@code androidx.startup}.
- *
- * @hide
*/
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkManagerInitializer implements Initializer<WorkManager> {
+public final class WorkManagerInitializer implements Initializer<WorkManager> {
private static final String TAG = Logger.tagWithPrefix("WrkMgrInitializer");
diff --git a/work/workmanager/src/main/java/androidx/work/WorkerParameters.java b/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
index 6580b99..a4c07e9 100644
--- a/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
+++ b/work/workmanager/src/main/java/androidx/work/WorkerParameters.java
@@ -195,6 +195,14 @@
}
/**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @NonNull RuntimeExtras getRuntimeExtras() {
+ return mRuntimeExtras;
+ }
+
+ /**
* Extra runtime information for Workers.
*
* @hide
diff --git a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
index 7074358..26654a4 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/WorkerWrapper.java
@@ -434,9 +434,8 @@
// Check to see if there is more work to be done. If there is no more work, then
// disable RescheduleReceiver. Using a transaction here, as there could be more than
// one thread looking at the list of eligible WorkSpecs.
- List<String> unfinishedWork = mWorkDatabase.workSpecDao().getAllUnfinishedWork();
- boolean noMoreWork = unfinishedWork == null || unfinishedWork.isEmpty();
- if (noMoreWork) {
+ boolean hasUnfinishedWork = mWorkDatabase.workSpecDao().hasUnfinishedWork();
+ if (!hasUnfinishedWork) {
PackageManagerHelper.setComponentEnabled(
mAppContext, RescheduleReceiver.class, false);
}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 224d23c..ac259b5 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -18,9 +18,12 @@
import static androidx.annotation.VisibleForTesting.PACKAGE_PRIVATE;
+import android.annotation.SuppressLint;
import android.app.job.JobInfo;
import android.content.ComponentName;
import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
import android.os.Build;
import android.os.PersistableBundle;
@@ -68,16 +71,16 @@
*/
JobInfo convert(WorkSpec workSpec, int jobId) {
Constraints constraints = workSpec.constraints;
- int jobInfoNetworkType = convertNetworkType(constraints.getRequiredNetworkType());
PersistableBundle extras = new PersistableBundle();
extras.putString(EXTRA_WORK_SPEC_ID, workSpec.id);
extras.putBoolean(EXTRA_IS_PERIODIC, workSpec.isPeriodic());
JobInfo.Builder builder = new JobInfo.Builder(jobId, mWorkServiceComponent)
- .setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(constraints.requiresCharging())
.setRequiresDeviceIdle(constraints.requiresDeviceIdle())
.setExtras(extras);
+ setRequiredNetwork(builder, constraints.getRequiredNetworkType());
+
if (!constraints.requiresDeviceIdle()) {
// Device Idle and Backoff Criteria cannot be set together
int backoffPolicy = workSpec.backoffPolicy == BackoffPolicy.LINEAR
@@ -131,11 +134,34 @@
}
/**
+ * Adds the required network capabilities on the {@link JobInfo.Builder} instance.
+ *
+ * @param builder The instance of {@link JobInfo.Builder}.
+ * @param networkType The {@link NetworkType} instance.
+ */
+ @SuppressLint("UnsafeNewApiCall")
+ static void setRequiredNetwork(
+ @NonNull JobInfo.Builder builder,
+ @NonNull NetworkType networkType) {
+
+ if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED) {
+ NetworkRequest networkRequest = new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED)
+ .build();
+
+ builder.setRequiredNetwork(networkRequest);
+ } else {
+ builder.setRequiredNetworkType(convertNetworkType(networkType));
+ }
+ }
+
+ /**
* Converts {@link NetworkType} into {@link JobInfo}'s network values.
*
* @param networkType The {@link NetworkType} network type
* @return The {@link JobInfo} network type
*/
+ @SuppressWarnings("MissingCasesInEnumSwitch")
static int convertNetworkType(NetworkType networkType) {
switch(networkType) {
case NOT_REQUIRED:
diff --git a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
index 799a112..1672f94 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
@@ -16,9 +16,11 @@
package androidx.work.impl.constraints.controllers;
+import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
import static androidx.work.NetworkType.UNMETERED;
import android.content.Context;
+import android.os.Build;
import androidx.annotation.NonNull;
import androidx.work.impl.constraints.NetworkState;
@@ -39,7 +41,9 @@
@Override
boolean hasConstraint(@NonNull WorkSpec workSpec) {
- return workSpec.constraints.getRequiredNetworkType() == UNMETERED;
+ return workSpec.constraints.getRequiredNetworkType() == UNMETERED
+ || (Build.VERSION.SDK_INT >= 30
+ && workSpec.constraints.getRequiredNetworkType() == TEMPORARILY_UNMETERED);
}
@Override
diff --git a/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java b/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
index 261e8e2..8bafccf 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/diagnostics/DiagnosticsReceiver.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
import androidx.work.Logger;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
@@ -29,7 +30,10 @@
/**
* The {@link android.content.BroadcastReceiver} which dumps out useful diagnostics information.
+ *
+ * @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class DiagnosticsReceiver extends BroadcastReceiver {
private static final String TAG = Logger.tagWithPrefix("DiagnosticsRcvr");
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
index 68b0e28..aa2f15f 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkSpecDao.java
@@ -265,6 +265,12 @@
List<String> getAllUnfinishedWork();
/**
+ * @return {@code true} if there is pending work.
+ */
+ @Query("SELECT COUNT(*) > 0 FROM workspec WHERE state NOT IN " + COMPLETED_STATES + " LIMIT 1")
+ boolean hasUnfinishedWork();
+
+ /**
* Marks a {@link WorkSpec} as scheduled.
*
* @param id The identifier for the {@link WorkSpec}
diff --git a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
index 2cdd930..4e665ea 100644
--- a/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ b/work/workmanager/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
@@ -26,6 +26,7 @@
import static androidx.work.WorkInfo.State.SUCCEEDED;
import android.net.Uri;
+import android.os.Build;
import androidx.room.TypeConverter;
import androidx.work.BackoffPolicy;
@@ -76,6 +77,7 @@
int UNMETERED = 2;
int NOT_ROAMING = 3;
int METERED = 4;
+ int TEMPORARILY_UNMETERED = 5;
}
/**
@@ -193,6 +195,7 @@
* @return The associated int constant
*/
@TypeConverter
+ @SuppressWarnings("NewApi")
public static int networkTypeToInt(NetworkType networkType) {
switch (networkType) {
case NOT_REQUIRED:
@@ -211,8 +214,13 @@
return NetworkTypeIds.METERED;
default:
+ if (Build.VERSION.SDK_INT >= 30
+ && networkType == NetworkType.TEMPORARILY_UNMETERED) {
+ return NetworkTypeIds.TEMPORARILY_UNMETERED;
+ }
throw new IllegalArgumentException(
"Could not convert " + networkType + " to int");
+
}
}
@@ -241,6 +249,9 @@
return NetworkType.METERED;
default:
+ if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
+ return NetworkType.TEMPORARILY_UNMETERED;
+ }
throw new IllegalArgumentException(
"Could not convert " + value + " to NetworkType");
}