Merge "Change most of StringDef enum like values to intdef." into androidx-main
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index d086cea..27fd54e 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -9,7 +9,6 @@
android {
defaultConfig {
multiDexEnabled true
- testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
}
namespace "androidx.activity"
}
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index f773be2..ab89766 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
@@ -30,12 +30,18 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class ComponentActivityCallbacksTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun onConfigurationChanged() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
index 56ed97d..2a23d40 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityLifecycleTest.kt
@@ -24,6 +24,8 @@
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,6 +39,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityLifecycleTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
@Throws(Throwable::class)
fun testLifecycleObserver() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
index 4062812..f72721a 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityMenuTest.kt
@@ -44,6 +44,8 @@
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -53,6 +55,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityMenuTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun inflatesMenu() {
withUse(ActivityScenario.launch(ComponentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
index 8e67e4f..23758b0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityOverrideLifecycleTest.kt
@@ -25,7 +25,9 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,6 +35,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityOverrideLifecycleTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@UiThreadTest
@Test
fun testEagerOverride() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
index c0a91ed..a091c94 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityReportFullyDrawnTest.kt
@@ -20,6 +20,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import androidx.testutils.withUse
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,6 +30,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityReportFullyDrawnTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testReportFullyDrawn() {
withUse(ActivityScenario.launch(ReportFullyDrawnActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
index ecbd905..e2025c9 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityResultTest.kt
@@ -31,6 +31,8 @@
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Test
@@ -39,6 +41,10 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
class ComponentActivityResultTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun launchInOnCreate() {
ActivityScenario.launch(ResultComponentActivity::class.java).use { scenario ->
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
index e89edfa..38fe672 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityRunOnNextRecreateTest.kt
@@ -26,6 +26,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,6 +35,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityRunOnNextRecreateTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
private class Restarted2 : SavedStateRegistry.AutoRecreated {
override fun onRecreated(owner: SavedStateRegistryOwner) {
(owner as? AutoRestarterActivity)?.restartedValue = "restarted"
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
index 238b742..0222cd4 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivitySavedStateTest.kt
@@ -27,7 +27,9 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.After
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +37,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivitySavedStateTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@After
fun clear() {
SavedStateActivity.checkEnabledInOnCreate = false
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
index 4b9b9e6..cd451a7 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityViewModelTest.kt
@@ -31,6 +31,8 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,6 +40,9 @@
@RunWith(AndroidJUnit4::class)
class ComponentActivityViewModelTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test(expected = IllegalStateException::class)
@UiThreadTest
fun testNotAttachedActivity() {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
index 5eaa89cb..b3a2567 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentDialogTest.kt
@@ -26,6 +26,8 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,6 +35,10 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
class ComponentDialogTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testLifecycle() {
withUse(ActivityScenario.launch(EmptyContentActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
index 1908896..925adcd 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ContentViewTest.kt
@@ -31,6 +31,8 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,6 +40,9 @@
@RunWith(AndroidJUnit4::class)
class ContentViewTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testLifecycleObserver() {
withUse(ActivityScenario.launch(ContentViewActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt b/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
index 2d7ff0a..1562394 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/FullyDrawnReporterTest.kt
@@ -32,6 +32,8 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -39,6 +41,10 @@
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
class FullyDrawnReporterTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun findFullyDrawnReporterOwner() {
withUse(ActivityScenario.launch(FullyDrawnActivity::class.java)) {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
index f324886..19d07fd 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/LeakInputMethodManagerTest.kt
@@ -25,20 +25,26 @@
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class LeakInputMethodManagerTest {
@Suppress("DEPRECATION")
- @get:Rule
val activityRule = androidx.test.rule.ActivityTestRule(LeakingActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun leakThroughRemovedEditText() {
activityRule.runOnUiThread {
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
index 5e1790d..1ff31b7 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherInvokerTest.kt
@@ -24,6 +24,8 @@
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,6 +35,9 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.S_V2)
class OnBackPressedDispatcherInvokerTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testSimpleInvoker() {
var registerCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
index 3a75468..7380772 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/OnBackPressedDispatcherTest.kt
@@ -26,7 +26,9 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,6 +39,9 @@
private var fallbackCount = 0
lateinit var dispatcher: OnBackPressedDispatcher
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Before
fun setup() {
fallbackCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
index 98a312c..f78e6ea 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ViewTreeOnBackPressedDispatcherTest.kt
@@ -23,12 +23,18 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class ViewTreeOnBackPressedDispatcherTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
/**
* Tests that a direct set/get on a single view survives a round trip
*/
diff --git a/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt b/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
index 424d646..e3bbb87 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/contextaware/ContextAwareHelperTest.kt
@@ -21,6 +21,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -29,6 +31,9 @@
class ContextAwareHelperTest {
private val contextAware = TestContextAware()
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun addOnContextAvailableListener() {
var callbackCount = 0
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
index f6240cc..3ef8476 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultLauncherTest.kt
@@ -26,6 +26,8 @@
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,6 +49,9 @@
}
}
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun launchTest() {
val launcher = registry.register("key", StartActivityForResult()) {}
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
index 03474b9..dad575f 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/ActivityResultRegistryTest.kt
@@ -29,7 +29,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -47,6 +49,9 @@
}
}
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testRegisterLifecycleOwnerCallback() {
val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.INITIALIZED)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
index 42992fb..fde1fa0 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/result/PickVisualMediaRequestTest.kt
@@ -20,6 +20,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -27,6 +29,9 @@
@RunWith(AndroidJUnit4::class)
class PickVisualMediaRequestTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun buildPickVisualMedia() {
val request = PickVisualMediaRequest.Builder().setMediaType(
diff --git a/activity/integration-tests/testapp/build.gradle b/activity/integration-tests/testapp/build.gradle
index 733b7e4..b8d1e1f 100644
--- a/activity/integration-tests/testapp/build.gradle
+++ b/activity/integration-tests/testapp/build.gradle
@@ -22,8 +22,6 @@
android {
defaultConfig {
- testInstrumentationRunnerArgument "listener", "com.squareup.leakcanary.FailTestOnLeakRunListener"
-
applicationId "androidx.activity.integration.testapp"
}
namespace "androidx.activity.integration.testapp"
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 6b454fa..c188aea 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -54,7 +54,6 @@
exclude group: "androidx.appcompat", module: "appcompat"
exclude group: "androidx.core", module: "core"
})
- androidTestImplementation(projectOrArtifact(":recyclerview:recyclerview"))
androidTestImplementation(libs.multidex)
testImplementation(libs.kotlinStdlib)
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
index 863853d..e126d37 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/BaseBasicsTestCase.java
@@ -56,6 +56,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -80,6 +81,7 @@
mActivityTestRule.getActivity().getSupportActionBar());
}
+ @Ignore // b/256195085
@Test
public void testActionBarOverflowVisibilityListener() {
if ("ranchu".equals(Build.HARDWARE)) {
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
index 5bd08dc..3e48121 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesUpdateTestCase.kt
@@ -26,6 +26,7 @@
import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWait
import androidx.appcompat.testutils.LocalesUtils.setLocalesAndWaitForRecreate
import androidx.core.os.LocaleListCompat
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.testutils.waitForExecution
@@ -118,6 +119,7 @@
@SdkSuppress(minSdkVersion = 17)
@Test
+ @FlakyTest(bugId = 255765202)
fun testLayoutDirectionAfterRecreating() {
setLocalesAndWaitForRecreate(rule, getRTLLocaleList())
diff --git a/benchmark/benchmark-darwin-gradle-plugin/build.gradle b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
index 4282513..ee36734 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/build.gradle
+++ b/benchmark/benchmark-darwin-gradle-plugin/build.gradle
@@ -25,6 +25,7 @@
dependencies {
api(gradleApi())
+ implementation(libs.kotlinGradlePluginz)
implementation(libs.gson)
implementation(libs.apacheCommonsMath)
testImplementation(gradleTestKit())
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index b712be2..13f1350 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -16,8 +16,12 @@
package androidx.benchmark.darwin.gradle
+import java.util.concurrent.atomic.AtomicBoolean
import org.gradle.api.Plugin
import org.gradle.api.Project
+import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
+import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
/**
* The Darwin benchmark plugin that helps run KMP benchmarks on iOS devices, and extracts benchmark
@@ -28,6 +32,28 @@
val extension =
project.extensions.create("darwinBenchmark", DarwinBenchmarkPluginExtension::class.java)
+ val appliedDarwinPlugin = AtomicBoolean()
+
+ project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {
+ val multiplatformExtension: KotlinMultiplatformExtension =
+ project.extensions.getByType(it.projectExtensionClass.java)
+ multiplatformExtension.targets.all { kotlinTarget ->
+ if (kotlinTarget is KotlinNativeTarget) {
+ if (kotlinTarget.konanTarget.family.isAppleFamily) {
+ // We want to apply the plugin only once.
+ if (appliedDarwinPlugin.compareAndSet(false, true)) {
+ applyDarwinPlugin(extension, project)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun applyDarwinPlugin(
+ extension: DarwinBenchmarkPluginExtension,
+ project: Project
+ ) {
val xcodeProjectPath = extension.xcodeProjectName.flatMap { name ->
project.layout.buildDirectory.dir("$name.xcodeproj")
}
@@ -51,13 +77,10 @@
it.xcodeProjectPath.set(generateXCodeProjectTask.flatMap { task ->
task.xcProjectPath
})
- val taskName = extension.xcFrameworkConfig.map { name ->
- "assemble${name}ReleaseXCFramework"
- }
it.destination.set(extension.destination)
it.scheme.set(extension.scheme)
it.xcResultPath.set(xcResultPath)
- it.dependsOn(taskName)
+ it.dependsOn(XCFRAMEWORK_TASK_NAME)
}
project.tasks.register(
@@ -78,5 +101,9 @@
const val GENERATE_XCODE_PROJECT_TASK = "generateXCodeProject"
const val RUN_DARWIN_BENCHMARKS_TASK = "runDarwinBenchmarks"
const val DARWIN_BENCHMARK_RESULTS_TASK = "darwinBenchmarkResults"
+
+ // There is always a XCFrameworkConfig with the name `AndroidXDarwinBenchmarks`
+ // The corresponding task name is `assemble{name}ReleaseXCFramework`
+ const val XCFRAMEWORK_TASK_NAME = "assembleAndroidXDarwinBenchmarksReleaseXCFramework"
}
}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
index 83781e8..35fec87 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPluginExtension.kt
@@ -43,10 +43,4 @@
* This is typically discovered by using `xcrun xctrace list devices`.
*/
abstract val destination: Property<String>
-
- /**
- * The name of the `XCFrameworkConfig` defined in the benchmark's module build.gradle.
- * This is used to derive the name of the `release` task to generate the `XCFramework`.
- */
- abstract val xcFrameworkConfig: Property<String>
}
diff --git a/benchmark/benchmark-darwin-samples/build.gradle b/benchmark/benchmark-darwin-samples/build.gradle
index da95b27..dc8c184 100644
--- a/benchmark/benchmark-darwin-samples/build.gradle
+++ b/benchmark/benchmark-darwin-samples/build.gradle
@@ -7,8 +7,7 @@
}
androidXMultiplatform {
-
- // XCFrameworkConfig must match module name
+ // XCFrameworkConfig must always be called AndroidXDarwinBenchmarks
def xcf = new XCFrameworkConfig(project, "AndroidXDarwinBenchmarks")
ios {
@@ -57,7 +56,6 @@
scheme = "testapp-ios"
// ios 13, 15.2
destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
- xcFrameworkConfig = "AndroidXDarwinBenchmarks"
}
androidx {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
index 602fcbc..05451ec 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/BaselineProfilesTest.kt
@@ -16,8 +16,11 @@
package androidx.benchmark.macro
+import androidx.benchmark.DeviceInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,4 +40,15 @@
assertEquals(filtered.lines().size, 1)
assertEquals("Landroidx/Foo/Bar;", filtered)
}
+
+ @Test
+ fun deviceSpecifier() {
+ if (DeviceInfo.isEmulator) {
+ assertEquals(deviceSpecifier, "-e ")
+ } else {
+ assertTrue(deviceSpecifier.startsWith("-s "), "observed $deviceSpecifier")
+ assertTrue(deviceSpecifier.endsWith(" "), "observed $deviceSpecifier")
+ assertNotEquals(deviceSpecifier, "-s ")
+ }
+ }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
index edfd61f..5d953d2 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ConfigurableActivity.kt
@@ -28,9 +28,10 @@
class ConfigurableActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val textContent = intent.getStringExtra(EXTRA_TEXT)
val view = TextView(this).apply {
- text = intent.getStringExtra(EXTRA_TEXT)
+ text = textContent
}
setContentView(view)
@@ -50,7 +51,11 @@
else -> {
// report delayed, modify text
val runnable = {
- view.text = FULLY_DRAWN_TEXT
+ view.text = if (textContent == INNER_ACTIVITY_TEXT) {
+ INNER_ACTIVITY_FULLY_DRAWN_TEXT
+ } else {
+ FULLY_DRAWN_TEXT
+ }
reportFullyDrawn()
}
view.postDelayed(runnable, reportFullyDrawnDelayMs)
@@ -76,6 +81,7 @@
private const val EXTRA_REPORT_FULLY_DRAWN_DELAY_MS = "REPORT_FULLY_DRAWN_DELAY_MS"
const val FULLY_DRAWN_TEXT = "FULLY DRAWN"
const val INNER_ACTIVITY_TEXT = "INNER ACTIVITY"
+ const val INNER_ACTIVITY_FULLY_DRAWN_TEXT = "INNER ACTIVITY FULLY DRAWN"
fun createIntent(
text: String,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 0815a2a..3fc5027 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -27,11 +27,13 @@
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
+import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull
+import kotlin.test.assertNull
import kotlin.test.assertTrue
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
import org.junit.Assert.fail
import org.junit.Assume.assumeTrue
import org.junit.Before
@@ -212,4 +214,58 @@
/** Tests getFrameStats after launch which does nothing, as Activity already visible */
@Test
fun getFrameStats_noop() = validateLaunchAndFrameStats(pressHome = false)
+
+ private fun validateShaderCache(empty: Boolean, packageName: String) {
+ val path = MacrobenchmarkScope.getShaderCachePath(packageName)
+ println("validating shader path $path")
+ val fileCount = Shell.executeScript("find $path -type f | wc -l").trim().toInt()
+ if (empty) {
+ assertEquals(0, fileCount)
+ } else {
+ assertNotEquals(0, fileCount)
+ }
+ }
+
+ private fun validateDropShaderCacheWithRoot(
+ dropShaderCacheBlock: MacrobenchmarkScope.() -> Unit
+ ) {
+ // need root to inspect target app's code cache dir, and emulators
+ // don't seem to store shaders
+ assumeTrue(Shell.isSessionRooted() && !DeviceInfo.isEmulator)
+
+ val scope = MacrobenchmarkScope(
+ Packages.TARGET,
+ launchWithClearTask = false
+ )
+ // reset to empty to begin with
+ scope.killProcess()
+ scope.dropShaderCacheBlock()
+ validateShaderCache(empty = true, scope.packageName)
+
+ // start an activity, expecting shader compilation
+ scope.pressHome()
+ // NOTE: if platform fixes default activity to not compile shaders,
+ // may need to update this test UI to trigger shader creation
+ scope.startActivityAndWait()
+ Thread.sleep(5000) // sleep to await flushing cache to disk
+ scope.killProcess()
+ validateShaderCache(empty = false, scope.packageName)
+
+ // verify deletion
+ scope.killProcess()
+ scope.dropShaderCacheBlock()
+ validateShaderCache(empty = true, scope.packageName)
+ }
+
+ @Test
+ fun dropShaderCacheBroadcast() = validateDropShaderCacheWithRoot {
+ // since this test runs on root and the public api falls back to
+ // a root impl, test the broadcast directly
+ assertNull(ProfileInstallBroadcast.dropShaderCache(packageName))
+ }
+
+ @Test
+ fun dropShaderCachePublicApi() = validateDropShaderCacheWithRoot {
+ dropShaderCache()
+ }
}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index c3fbdc2..892820b 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -129,10 +129,18 @@
scope.startActivityAndWait(launchIntent)
}
- if (delayMs > 0) {
+ if (useInAppNav) {
+ // in app nav destinations always have different strings to differentiate
+ // vs the first activity's strings to prevent races
+ awaitActivityText(
+ if (delayMs > 0) {
+ ConfigurableActivity.INNER_ACTIVITY_FULLY_DRAWN_TEXT
+ } else {
+ ConfigurableActivity.INNER_ACTIVITY_TEXT
+ }
+ )
+ } else if (delayMs > 0) {
awaitActivityText(ConfigurableActivity.FULLY_DRAWN_TEXT)
- } else if (useInAppNav) {
- awaitActivityText(ConfigurableActivity.INNER_ACTIVITY_TEXT)
}
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index e9e3b13..749f23e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -22,6 +22,7 @@
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import androidx.benchmark.Arguments
+import androidx.benchmark.DeviceInfo
import androidx.benchmark.InstrumentationResults
import androidx.benchmark.Outputs
import androidx.benchmark.Shell
@@ -285,7 +286,7 @@
.append(
"""
To copy the profile use:
- adb pull "${record.profilePath}" .
+ adb pull $deviceSpecifier"${record.profilePath}" .
""".trimIndent()
)
@@ -296,13 +297,31 @@
.append(
"""
To copy the startup profile use:
- adb pull "${record.startupProfilePath}" .
+ adb pull $deviceSpecifier"${record.startupProfilePath}" .
""".trimIndent()
)
}
return summary.toString()
}
+/**
+ * adb device specifier, blank if can't be defined. Includes right side space.
+ */
+internal val deviceSpecifier by lazy {
+ if (DeviceInfo.isEmulator) {
+ // emulators have serials that aren't usable via ADB -s,
+ // so we just specify emulator and hope there's only one
+ "-e "
+ } else {
+ val getpropOutput = Shell.executeScriptWithStderr("getprop ro.serialno")
+ if (getpropOutput.stdout.isBlank() || getpropOutput.stderr.isNotBlank()) {
+ "" // failed to get serial
+ } else {
+ "-s ${getpropOutput.stdout.trim()} "
+ }
+ }
+}
+
private data class Summary(
val totalRunTime: Long,
val profilePath: String,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 39cf318..285827b 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -28,16 +28,14 @@
import androidx.benchmark.macro.CompilationMode.Ignore
import androidx.benchmark.macro.CompilationMode.None
import androidx.benchmark.macro.CompilationMode.Partial
-import androidx.benchmark.userspaceTrace
import androidx.profileinstaller.ProfileInstallReceiver
import org.junit.AssumptionViolatedException
/**
* Type of compilation to use for a Macrobenchmark.
*
- * Every Macrobenchmark has compilation reset before running, so that previous runs do not interfere
- * with the next. This compilation mode dictates any pre-compilation that occurs before repeatedly
- * running the setup / measure blocks of the benchmark.
+ * This compilation mode controls pre-compilation that occurs before running the setup / measure
+ * blocks of the benchmark.
*
* On Android N+ (API 24+), there are different levels of compilation supported:
*
@@ -48,9 +46,12 @@
* guide pre-compilation to mimic an application's performance after some, and JIT-ing has occurred.
*
* * [Full] - the app is fully pre-compiled. This is generally not representative of real user
- * experience, as apps are not fully pre-compiled on user devices, but this can be used to either
- * illustrate ideal performance, or to reduce noise/inconsistency from just-in-time compilation
- * while the benchmark runs.
+ * experience, as apps are not fully pre-compiled on user devices more recent than Android N
+ * (API 24). `Full` can be used to show unrealistic but potentially more stable performance by
+ * removing the noise/inconsistency from just-in-time compilation within benchmark runs. Note that
+ * `Full` compilation will often be slower than [Partial] compilation, as the increased code size
+ * creates more cost for disk loading during startup, and increases pressure in the instruction
+ * cache.
*
* * [None] - the app isn't pre-compiled at all, bypassing the default compilation that should
* generally be done at install time, e.g. by the Play Store. This will illustrate worst case
@@ -58,7 +59,8 @@
* useful for judging the performance impact of the baseline profiles included in your application.
*
* * [Ignore] - the state of compilation will be ignored. The intended use-case is for a developer
- * to customize the compilation state for an app; and then tell Macrobenchmark to leave it unchanged.
+ * to customize the compilation state for an app; and then tell Macrobenchmark to leave it
+ * unchanged.
*
* On Android M (API 23), only [Full] is supported, as all apps are always fully compiled.
*
@@ -71,40 +73,12 @@
sealed class CompilationMode {
internal fun resetAndCompile(
packageName: String,
- usePackageReset: () -> Boolean = Shell::isSessionRooted,
killProcessBlock: () -> Unit,
warmupBlock: () -> Unit
) {
if (Build.VERSION.SDK_INT >= 24) {
if (Arguments.enableCompilation) {
Log.d(TAG, "Resetting $packageName")
- // The compilation mode chooses whether a reset is required or not.
- // Currently the only compilation mode that does not perform a reset is
- // CompilationMode.Ignore.
- if (shouldReset()) {
- // It's not possible to reset the compilation profile on `user` builds.
- // The flag `enablePackageReset` can be set to `true` on `userdebug` builds in
- // order to speed-up the profile reset. When set to false, reset is performed
- // uninstalling and reinstalling the app.
- if (usePackageReset()) {
- // Package reset enabled
- Log.d(TAG, "Re-compiling $packageName")
- // cmd package compile --reset returns a "Success" or a "Failure" to stdout.
- // Rather than rely on exit codes which are not always correct, we specifically
- // look for the work "Success" in stdout to make sure reset actually
- // happened.
- val output = Shell.executeScriptWithStderr(
- "cmd package compile --reset $packageName"
- )
- check(output.stdout.trim() == "Success") {
- "Unable to recompile $packageName ($output)"
- }
- } else {
- // User builds. Kick off a full uninstall-reinstall
- Log.d(TAG, "Reinstalling $packageName")
- reinstallPackage(packageName)
- }
- }
// Write skip file to stop profile installer from interfering with the benchmark
writeProfileInstallerSkipFile(packageName, killProcessBlock = killProcessBlock)
compileImpl(packageName, killProcessBlock, warmupBlock)
@@ -114,46 +88,6 @@
}
}
- // This is a more expensive when compared to `compile --reset`.
- private fun reinstallPackage(packageName: String) {
- userspaceTrace("reinstallPackage") {
- val packagePath = Shell.executeScript("pm path $packageName")
- // The result looks like: `package: <result>`
- val apkPath = packagePath.substringAfter("package:").trim()
- // Copy the APK to /data/local/temp
- val tempApkPath = "/data/local/tmp/$packageName-${System.currentTimeMillis()}.apk"
- Log.d(TAG, "Copying APK to $tempApkPath")
- val result = Shell.executeScriptWithStderr(
- "cp $apkPath $tempApkPath"
- )
- if (result.stderr.isNotBlank()) {
- Log.w(TAG, "Unable to copy apk ($result)")
- } else {
- try {
- // Uninstall package
- // This is what effectively clears the ART profiles
- Log.d(TAG, "Uninstalling $packageName")
- var output = Shell.executeScriptWithStderr("pm uninstall $packageName")
- check(output.stdout.trim() == "Success") {
- "Unable to uninstall $packageName ($result)"
- }
- // Install the APK from /data/local/tmp
- Log.d(TAG, "Installing $packageName")
- // Provide a `-t` argument to `pm install` to ensure test packages are
- // correctly installed. (b/231294733)
- output = Shell.executeScriptWithStderr("pm install -t $tempApkPath")
- check(output.stdout.trim() == "Success") {
- "Unable to install $packageName ($result)"
- }
- } finally {
- // Cleanup the temporary APK
- Log.d(TAG, "Deleting $tempApkPath")
- Shell.executeCommand("rm $tempApkPath")
- }
- }
- }
- }
-
/**
* Writes a skip file via a [ProfileInstallReceiver] broadcast, so profile installation
* does not interfere with benchmarks.
@@ -180,9 +114,6 @@
warmupBlock: () -> Unit
)
- @RequiresApi(24)
- internal abstract fun shouldReset(): Boolean
-
/**
* No pre-compilation - a compilation profile reset is performed and the entire app will be
* allowed to Just-In-Time compile as it runs.
@@ -200,10 +131,8 @@
killProcessBlock: () -> Unit,
warmupBlock: () -> Unit
) {
- // nothing to do!
+ cmdPackageCompile(packageName, "verify")
}
-
- override fun shouldReset(): Boolean = true
}
/**
@@ -223,8 +152,6 @@
) {
// Do nothing.
}
-
- override fun shouldReset(): Boolean = false
}
/**
@@ -335,8 +262,6 @@
cmdPackageCompile(packageName, "speed-profile")
}
}
-
- override fun shouldReset(): Boolean = true
}
/**
@@ -361,8 +286,6 @@
}
// Noop on older versions: apps are fully compiled at install time on API 23 and below
}
-
- override fun shouldReset(): Boolean = true
}
/**
@@ -386,8 +309,6 @@
) {
// Nothing to do - handled externally
}
-
- override fun shouldReset(): Boolean = true
}
companion object {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 5db44a4..ed2982f 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -377,13 +377,14 @@
// Run setup before killing process
setupBlock(this)
+ // Shader caches are stored in the code cache directory. Make sure that
+ // they are cleared every iteration. Must be done before kill, since on user builds
+ // this broadcasts to the target app
+ dropShaderCache()
+
// Kill - code below must not wake process!
killProcess()
- // Shader caches are stored in the code cache directory. Make sure that
- // they are cleared every iteration.
- dropShaderCache()
-
// Ensure app's pages are not cached in memory for a true _cold_ start.
dropKernelPageCache()
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 45a220b..7c21891 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -16,16 +16,19 @@
package androidx.benchmark.macro
+import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.benchmark.DeviceInfo
import androidx.benchmark.Shell
+import androidx.benchmark.macro.MacrobenchmarkScope.Companion.Api24Helper.shaderDir
import androidx.benchmark.macro.perfetto.forceTrace
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.tracing.trace
+import java.io.File
/**
* Provides access to common operations in app automation, such as killing the app,
@@ -225,19 +228,30 @@
}
/**
- * Deletes the Shader cache for an application.
+ * Deletes the shader cache for an application.
*
- * Enables measurement of shader-cache startup cost during
- * [cold starts](androidx.benchmark.macro.StartupMode.COLD).
+ * Used by `measureRepeated(startupMode = StartupMode.COLD)` to remove compiled shaders for each
+ * measurement, to ensure their cost is captured each time.
+ *
+ * Requires `profileinstaller` 1.3.0-alpha02 to be used by the target, or a rooted device.
+ *
+ * @throws IllegalStateException if the device is not rooted, and the target app cannot be
+ * signalled to drop its shader cache.
*/
public fun dropShaderCache() {
Log.d(TAG, "Dropping shader cache for $packageName")
- // Shader cache is stored in the codeCacheDirectory
- // https://source.corp.google.com/android-internal/frameworks/base/core/java/android/app/ActivityThread.java;l=6410
- val shaderCachePath = instrumentation.targetContext.codeCacheDir.absolutePath
- val output = Shell.executeScript("rm -rf $shaderCachePath")
- check(output.isBlank()) {
- "Unable to drop shader cache for $packageName ($output)"
+ val dropError = ProfileInstallBroadcast.dropShaderCache(packageName)
+ if (dropError != null) {
+ if (Shell.isSessionRooted()) {
+ // fall back to root approach
+ val path = getShaderCachePath(packageName)
+ val output = Shell.executeScriptWithStderr("find $path -type f | xargs rm")
+ check(output.isBlank()) {
+ "Failed to delete shader cache directory $path, output $output"
+ }
+ } else {
+ throw IllegalStateException(dropError)
+ }
}
}
@@ -294,4 +308,27 @@
}
}
}
+
+ internal companion object {
+ fun getShaderCachePath(packageName: String): String {
+ val context = InstrumentationRegistry.getInstrumentation().context
+
+ // Shader paths sourced from ActivityThread.java
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ context.shaderDir
+ } else {
+ // getCodeCacheDir was added in L, but not used by platform for shaders until M
+ // as M is minApi of this library, that's all we support here
+ context.codeCacheDir
+ }.absolutePath.replace(context.packageName, packageName)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ internal object Api24Helper {
+ val Context.shaderDir: File
+ get() =
+ // shaders started using device protected storage context once it was added in N
+ createDeviceProtectedStorageContext().codeCacheDir
+ }
+ }
}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index 07215af..1e2eb91 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -171,4 +171,50 @@
}
}
}
+
+ private fun benchmarkOperation(packageName: String, operation: String): String? {
+ Log.d(TAG, "Profile Installer - Benchmark Operation: $operation")
+ // Redefining constants here, because these are only defined in the latest alpha for
+ // ProfileInstaller.
+ // Use an explicit broadcast given the app was force-stopped.
+ val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
+ val operationKey = "EXTRA_BENCHMARK_OPERATION"
+ val extras = "$operationKey $operation"
+ val result = Shell.executeCommand(
+ "am broadcast -a $action -e $extras $packageName/$receiverName"
+ )
+ .substringAfter("Broadcast completed: result=")
+ .trim()
+ .toIntOrNull()
+ return when (result) {
+ null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
+ // 0 is returned by the platform by default, and also if no broadcast receiver
+ // receives the broadcast.
+
+ // NOTE: may need to update this over time for different versions,
+ // based on operation string
+ "The $operation broadcast was not received. " +
+ "This most likely means that the `androidx.profileinstaller` library " +
+ "used by the target apk is old. Please use `1.3.0-alpha02` or newer. " +
+ "For more information refer to the release notes at " +
+ "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
+ }
+ 15 -> { // RESULT_BENCHMARK_OPERATION_FAILURE
+ "The $operation broadcast failed."
+ }
+ 14 -> { // RESULT_BENCHMARK_OPERATION_SUCCESS
+ null // success!
+ }
+ else -> {
+ throw RuntimeException(
+ "unrecognized ProfileInstaller result code: $result"
+ )
+ }
+ }
+ }
+
+ fun dropShaderCache(packageName: String): String? = benchmarkOperation(
+ packageName,
+ "DROP_SHADER_CACHE"
+ )
}
\ No newline at end of file
diff --git a/benchmark/gradle-plugin/build.gradle b/benchmark/gradle-plugin/build.gradle
index 2ef57d1..9bbf2cc 100644
--- a/benchmark/gradle-plugin/build.gradle
+++ b/benchmark/gradle-plugin/build.gradle
@@ -32,7 +32,6 @@
testImplementation(gradleTestKit())
testImplementation(project(":internal-testutils-gradle-plugin"))
- testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
}
@@ -71,4 +70,4 @@
failOnWarning.set(true)
enableStricterValidation.set(true)
}
-}
\ No newline at end of file
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 727552a..15d46bf 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -148,6 +148,10 @@
project.configurations.create("samples")
project.validateMultiplatformPluginHasNotBeenApplied()
+
+ if (!ProjectLayoutType.isPlayground(project)) {
+ project.whenChangingOutputTextValidationMustInvalidateAllTasks()
+ }
}
private fun Project.registerProjectOrArtifact() {
@@ -1083,6 +1087,17 @@
}
}
+// If our output message validation configuration changes, invalidate all tasks to make sure
+// all output messages get regenerated and re-validated
+private fun Project.whenChangingOutputTextValidationMustInvalidateAllTasks() {
+ val configFile = project.rootProject.file("development/build_log_simplifier/messages.ignore")
+ if (configFile.exists()) {
+ project.tasks.configureEach { task ->
+ task.inputs.file(configFile)
+ }
+ }
+}
+
/**
* Validates the Maven version against Jetpack guidelines.
*/
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 672f99b..963ed8d 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -220,28 +220,14 @@
registerStudioTask()
- if (!ProjectLayoutType.isPlayground(project)) {
- whenChangingOutputTextValidationMustInvalidateAllTasks()
- }
-
TaskUpToDateValidator.setup(project, registry)
project.tasks.register("listTaskOutputs", ListTaskOutputsTask::class.java) { task ->
task.setOutput(File(project.getDistributionDirectory(), "task_outputs.txt"))
task.removePrefix(project.getCheckoutRoot().path)
}
- }
-
- // If our output message validation configuration changes, invalidate all tasks to make sure
- // all output messages get regenerated and re-validated
- private fun Project.whenChangingOutputTextValidationMustInvalidateAllTasks() {
- val configFile = project.file("development/build_log_simplifier/messages.ignore")
- if (configFile.exists()) {
- subprojects { subproject ->
- subproject.tasks.configureEach { task ->
- task.inputs.file(configFile)
- }
- }
+ tasks.matching { it.name == "commonizeNativeDistribution" }.configureEach {
+ it.notCompatibleWithConfigurationCache("https://youtrack.jetbrains.com/issue/KT-54627")
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
index 08aedba..4e9b6b1e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ListTaskOutputsTask.kt
@@ -22,15 +22,15 @@
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
-import org.gradle.work.DisableCachingByDefault
/**
* Finds the outputs of every task and saves this mapping into a file
*/
-@DisableCachingByDefault(because = "Uses too many inputs to be feasible to cache, but runs quickly")
+@CacheableTask
abstract class ListTaskOutputsTask : DefaultTask() {
@OutputFile
val outputFile: Property<File> = project.objects.property(File::class.java)
@@ -39,12 +39,14 @@
@Input
val tasks: MutableList<Task> = mutableListOf()
+ @get:Input
+ val outputText by lazy { computeOutputText() }
+
init {
group = "Help"
- outputs.upToDateWhen { false }
- notCompatibleWithConfigurationCache(
- "This task uses project object to inspect outputs of all tasks"
- )
+ // compute the output text when the taskgraph is ready so that the output text can be
+ // saved in the configuration cache and not generate a configuration cache violation
+ project.gradle.taskGraph.whenReady({ outputText.toString() })
}
fun setOutput(f: File) {
@@ -94,14 +96,15 @@
return components.joinToString("")
}
+ fun computeOutputText(): String {
+ val tasksByOutput = project.rootProject.findAllTasksByOutput()
+ return formatTasks(tasksByOutput)
+ }
+
@TaskAction
fun exec() {
- val tasksByOutput = project.rootProject.findAllTasksByOutput()
- val text = formatTasks(tasksByOutput)
-
val outputFile = outputFile.get()
- outputFile.writeText(text)
- logger.lifecycle("Wrote ${outputFile.path}")
+ outputFile.writeText(outputText)
}
}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
index 6ad30d2..844ee8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/license/CheckExternalDependencyLicensesTask.kt
@@ -15,7 +15,6 @@
*/
package androidx.build.license
-import androidx.build.enforceKtlintVersion
import androidx.build.getPrebuiltsRoot
import java.io.File
import org.gradle.api.DefaultTask
@@ -115,45 +114,49 @@
task.filesToCheck.from(
project.provider {
- val checkerConfig = project.configurations.detachedConfiguration()
- checkerConfig.isCanBeConsumed = false
- checkerConfig.attributes {
- it.attribute(
- Usage.USAGE_ATTRIBUTE,
- project.objects.named<Usage>(Usage.JAVA_RUNTIME)
- )
- it.attribute(
- Category.CATEGORY_ATTRIBUTE,
- project.objects.named<Category>(Category.LIBRARY)
- )
- it.attribute(
- GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
- project.objects.named<GradlePluginApiVersion>(
- GradleVersion.current().getVersion()
- )
- )
- }
- // workaround for b/234884534
- project.enforceKtlintVersion(checkerConfig)
- project
- .configurations
- .flatMap {
- it.allDependencies
- .filterIsInstance(ExternalDependency::class.java)
- .filterNot {
- it.group?.startsWith("com.android") == true
- }
- .filterNot {
- it.group?.startsWith("android.arch") == true
- }
- .filterNot {
- it.group?.startsWith("androidx") == true
- }
+ val configName = "CheckExternalLicences"
+ val container = project.configurations
+ val checkerConfig =
+ container.findByName(configName) ?: container.create(configName) { checkerConfig ->
+
+ checkerConfig.isCanBeConsumed = false
+ checkerConfig.attributes {
+ it.attribute(
+ Usage.USAGE_ATTRIBUTE,
+ project.objects.named<Usage>(Usage.JAVA_RUNTIME)
+ )
+ it.attribute(
+ Category.CATEGORY_ATTRIBUTE,
+ project.objects.named<Category>(Category.LIBRARY)
+ )
+ it.attribute(
+ GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
+ project.objects.named<GradlePluginApiVersion>(
+ GradleVersion.current().getVersion()
+ )
+ )
}
- .forEach {
- checkerConfig.dependencies.add(it)
- }
+
+ project
+ .configurations
+ .flatMap {
+ it.allDependencies
+ .filterIsInstance(ExternalDependency::class.java)
+ .filterNot {
+ it.group?.startsWith("com.android") == true
+ }
+ .filterNot {
+ it.group?.startsWith("android.arch") == true
+ }
+ .filterNot {
+ it.group?.startsWith("androidx") == true
+ }
+ }
+ .forEach {
+ checkerConfig.dependencies.add(it)
+ }
+ }
val localArtifactRepositories = project.findLocalMavenRepositories()
val dependencyArtifacts = checkerConfig.incoming.artifacts.artifacts.mapNotNull {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
index ce578e5..dfb282f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
@@ -24,11 +24,13 @@
import androidx.build.getSdkPath
import androidx.build.getSupportRootFolder
import com.android.build.gradle.BaseExtension
+import javax.inject.Inject
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.type.ArtifactTypeDefinition.ARTIFACT_TYPE_ATTRIBUTE
import org.gradle.api.artifacts.type.ArtifactTypeDefinition.JAR_TYPE
import org.gradle.api.file.FileCollection
+import org.gradle.api.file.FileSystemOperations
import org.gradle.api.tasks.PathSensitivity
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.the
@@ -38,7 +40,9 @@
/**
* Configures screenshot testing using Paparazzi for AndroidX projects.
*/
-class AndroidXPaparazziImplPlugin : Plugin<Project> {
+class AndroidXPaparazziImplPlugin @Inject constructor(
+ private val fileSystemOperations: FileSystemOperations
+) : Plugin<Project> {
override fun apply(project: Project) {
val paparazziNative = project.createUnzippedPaparazziNativeDependency()
project.afterEvaluate {
@@ -53,6 +57,7 @@
private fun Test.configureTestTask(paparazziNative: FileCollection) {
val platformDirectory = project.getSdkPath().resolve("platforms/$COMPILE_SDK_VERSION")
val goldenRootDirectory = project.getSupportRootFolder().resolve("../../golden")
+ val reportDirectory = project.buildDir.resolve("paparazzi").resolve(name)
val modulePath = project.path.replace(':', '/').trim('/')
val android = project.the<BaseExtension>()
val packageName = requireNotNull(android.namespace) {
@@ -69,6 +74,13 @@
.withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("goldenDirectory")
+ // Mark report directory as an output directory
+ outputs.dir(reportDirectory)
+ .withPropertyName("paparazziReportDir")
+
+ // Clean the contents of the report directory before each test run
+ doFirst { fileSystemOperations.delete { it.delete(reportDirectory.listFiles()) } }
+
// Set non-path system properties at configuration time, so that changes invalidate caching
prefixedSystemProperties(
"gradlePluginApplied" to "true",
@@ -82,10 +94,10 @@
doFirst {
systemProperty("paparazzi.platform.data.root", paparazziNative.singleFile.canonicalPath)
prefixedSystemProperties(
- "platformDir" to platformDirectory,
+ "platformDir" to platformDirectory.canonicalPath,
"assetsDir" to ".", // TODO: Merged assets dirs? (needed for compose?)
"resDir" to ".", // TODO: Merged resource dirs? (needed for compose?)
- "reportDir" to reports.junitXml.outputLocation.get().asFile.canonicalPath,
+ "reportDir" to reportDirectory.canonicalPath,
"goldenRootDir" to goldenRootDirectory.canonicalPath,
)
}
diff --git a/busytown/androidx_host_tests.sh b/busytown/androidx_host_tests.sh
index bba9e74..00ba171 100755
--- a/busytown/androidx_host_tests.sh
+++ b/busytown/androidx_host_tests.sh
@@ -5,9 +5,10 @@
cd "$(dirname $0)"
-impl/build.sh test \
+impl/build.sh test linuxX64Test \
-Pandroidx.ignoreTestFailures \
-Pandroidx.displayTestOutput=false \
+ -Pandroidx.enabled.kmp.target.platforms=+native \
"$@"
echo "Completing $0 at $(date)"
diff --git a/busytown/androidx_host_tests_max_dep_versions.sh b/busytown/androidx_host_tests_max_dep_versions.sh
index 75d276e..a56dbc4 100755
--- a/busytown/androidx_host_tests_max_dep_versions.sh
+++ b/busytown/androidx_host_tests_max_dep_versions.sh
@@ -5,8 +5,9 @@
cd "$(dirname $0)"
-impl/build.sh test -Pandroidx.useMaxDepVersions \
+impl/build.sh test linuxX64Test -Pandroidx.useMaxDepVersions \
-Pandroidx.displayTestOutput=false \
+ -Pandroidx.enabled.kmp.target.platforms=+native \
-Pandroidx.ignoreTestFailures "$@"
echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
index 46a29ef9..65e72dc 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
@@ -208,8 +208,7 @@
// Assert, verify the usage count of the DeferrableSurface
assertThat(cameraOpenedUsageCount).isEqualTo(2)
- // TODO(lnishan): After aosp/2241588, cameraDisconnectedUsageCount should remain at 2
- assertThat(cameraDisconnectedUsageCount).isGreaterThan(0)
+ assertThat(cameraDisconnectedUsageCount).isEqualTo(2)
assertThat(cameraClosedUsageCount).isEqualTo(1)
}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
index 3043278..b4bcd62 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -26,6 +26,7 @@
import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraConfigs
import androidx.camera.core.impl.CameraControlInternal
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CameraInternal
@@ -50,6 +51,8 @@
private val threads: UseCaseThreads,
) : CameraInternal {
private val cameraId = config.cameraId
+ private var coreCameraConfig: androidx.camera.core.impl.CameraConfig =
+ CameraConfigs.emptyConfig()
private val debugId = cameraAdapterIds.incrementAndGet()
private val cameraState = LiveDataObservable<CameraInternal.State>()
@@ -104,5 +107,13 @@
useCaseManager.deactivate(useCase)
}
+ override fun getExtendedConfig(): androidx.camera.core.impl.CameraConfig {
+ return coreCameraConfig
+ }
+
+ override fun setExtendedConfig(cameraConfig: androidx.camera.core.impl.CameraConfig?) {
+ coreCameraConfig = cameraConfig ?: CameraConfigs.emptyConfig()
+ }
+
override fun toString(): String = "CameraInternalAdapter<$cameraId>"
}
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
index 42550a1..1c6c3c2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
@@ -18,8 +18,10 @@
import android.view.Surface
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState
import androidx.camera.camera2.pipe.StreamGraph
import androidx.camera.camera2.pipe.StreamId
+import kotlinx.coroutines.flow.StateFlow
class FakeCameraGraph(
val fakeCameraGraphSession: FakeCameraGraphSession = FakeCameraGraphSession()
@@ -30,6 +32,9 @@
override val streams: StreamGraph
get() = throw NotImplementedError("Not used in testing")
+ override val graphState: StateFlow<GraphState>
+ get() = throw NotImplementedError("Not used in testing")
+
override suspend fun acquireSession(): CameraGraph.Session {
return fakeCameraGraphSession
}
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 6c03ad4..740146a 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
@@ -28,6 +28,32 @@
import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
import java.io.Closeable
import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.flow.StateFlow
+
+abstract class GraphState internal constructor() {
+ /**
+ * When the [CameraGraph] is starting. This means we're in the process of opening a (virtual)
+ * camera and creating a capture session.
+ */
+ object GraphStateStarting : GraphState()
+
+ /**
+ * When the [CameraGraph] is started. This means a capture session has been successfully created
+ * for the [CameraGraph].
+ */
+ object GraphStateStarted : GraphState()
+
+ /**
+ * When the [CameraGraph] is stopping. This means we're in the process of stopping the graph.
+ */
+ object GraphStateStopping : GraphState()
+
+ /**
+ * When the [CameraGraph] hasn't been started, or stopped. This does not guarantee the closure
+ * of the capture session or the camera device itself.
+ */
+ object GraphStateStopped : GraphState()
+}
/**
* A [CameraGraph] represents the combined configuration and state of a camera.
@@ -37,6 +63,12 @@
public val streams: StreamGraph
/**
+ * Returns the state flow of [GraphState], which emits the current state of the
+ * [CameraGraph], including when a [CameraGraph] is stopped, starting or started.
+ */
+ public val graphState: StateFlow<GraphState>
+
+ /**
* This will cause the [CameraGraph] to start opening the [CameraDevice] and configuring a
* [CameraCaptureSession]. While the CameraGraph is alive it will attempt to keep the camera
* open, active, and in a configured running state.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
index 44e6b3b..69f3df0 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -78,10 +78,20 @@
camera: CameraId? = null,
outputType: OutputStream.OutputType = OutputStream.OutputType.SURFACE,
mirrorMode: OutputStream.MirrorMode? = null,
- timestampBase: OutputStream.TimestampBase? = null
+ timestampBase: OutputStream.TimestampBase? = null,
+ dynamicRangeProfile: OutputStream.DynamicRangeProfile? = null,
+ streamUseCase: OutputStream.StreamUseCase? = null,
): Config = create(
- OutputStream.Config
- .create(size, format, camera, outputType, mirrorMode, timestampBase)
+ OutputStream.Config.create(
+ size,
+ format,
+ camera,
+ outputType,
+ mirrorMode,
+ timestampBase,
+ dynamicRangeProfile,
+ streamUseCase,
+ )
)
/**
@@ -124,6 +134,8 @@
public val camera: CameraId
public val mirrorMode: MirrorMode?
public val timestampBase: TimestampBase?
+ public val dynamicRangeProfile: DynamicRangeProfile?
+ public val streamUseCase: StreamUseCase?
// TODO: Consider adding sensor mode and/or other metadata
/**
@@ -135,6 +147,8 @@
public val camera: CameraId?,
public val mirrorMode: MirrorMode?,
public val timestampBase: TimestampBase?,
+ public val dynamicRangeProfile: DynamicRangeProfile?,
+ public val streamUseCase: StreamUseCase?,
) {
companion object {
fun create(
@@ -144,15 +158,34 @@
outputType: OutputType = OutputType.SURFACE,
mirrorMode: MirrorMode? = null,
timestampBase: TimestampBase? = null,
+ dynamicRangeProfile: DynamicRangeProfile? = null,
+ streamUseCase: StreamUseCase? = null,
): Config =
if (
outputType == OutputType.SURFACE_TEXTURE ||
outputType == OutputType.SURFACE_VIEW
) {
- LazyOutputConfig(size, format, camera, outputType, mirrorMode, timestampBase)
+ LazyOutputConfig(
+ size,
+ format,
+ camera,
+ outputType,
+ mirrorMode,
+ timestampBase,
+ dynamicRangeProfile,
+ streamUseCase
+ )
} else {
check(outputType == OutputType.SURFACE)
- SimpleOutputConfig(size, format, camera, mirrorMode, timestampBase)
+ SimpleOutputConfig(
+ size,
+ format,
+ camera,
+ mirrorMode,
+ timestampBase,
+ dynamicRangeProfile,
+ streamUseCase,
+ )
}
/** Create a stream configuration from an externally created [OutputConfiguration] */
@@ -176,7 +209,17 @@
camera: CameraId?,
mirrorMode: MirrorMode?,
timestampBase: TimestampBase?,
- ) : Config(size, format, camera, mirrorMode, timestampBase)
+ dynamicRangeProfile: DynamicRangeProfile?,
+ streamUseCase: StreamUseCase?,
+ ) : Config(
+ size,
+ format,
+ camera,
+ mirrorMode,
+ timestampBase,
+ dynamicRangeProfile,
+ streamUseCase
+ )
/**
* Used to configure an output with a surface that may be provided after the camera is running.
@@ -193,7 +236,17 @@
internal val outputType: OutputType,
mirrorMode: MirrorMode?,
timestampBase: TimestampBase?,
- ) : Config(size, format, camera, mirrorMode, timestampBase)
+ dynamicRangeProfile: DynamicRangeProfile?,
+ streamUseCase: StreamUseCase?,
+ ) : Config(
+ size,
+ format,
+ camera,
+ mirrorMode,
+ timestampBase,
+ dynamicRangeProfile,
+ streamUseCase,
+ )
/**
* Used to define an output that comes from an externally managed OutputConfiguration object.
@@ -209,8 +262,15 @@
format: StreamFormat,
camera: CameraId?,
val output: OutputConfiguration,
- ) : Config(size, format, camera, MirrorMode(Api33Compat.getMirrorMode(output)),
- TimestampBase(Api33Compat.getTimestampBase(output)))
+ ) : Config(
+ size,
+ format,
+ camera,
+ MirrorMode(Api33Compat.getMirrorMode(output)),
+ TimestampBase(Api33Compat.getTimestampBase(output)),
+ DynamicRangeProfile(Api33Compat.getDynamicRangeProfile(output)),
+ StreamUseCase(Api33Compat.getStreamUseCase(output)),
+ )
}
enum class OutputType {
@@ -254,6 +314,52 @@
val TIMESTAMP_BASE_CHOREOGRAPHER_SYNCED = TimestampBase(4)
}
}
+
+ /**
+ * Adds the ability to define the dynamic range profile of the OutputStream.
+ * [STANDARD] is the default dynamic range profile for the camera device, with which
+ * the camera device uses an 8-bit standard profile.
+ *
+ * See the documentation on [OutputConfiguration.setDynamicRangeProfile] for more details.
+ */
+ @JvmInline
+ value class DynamicRangeProfile(val value: Long) {
+ companion object {
+ val STANDARD = DynamicRangeProfile(1)
+ val HLG10 = DynamicRangeProfile(2)
+ val HDR10 = DynamicRangeProfile(4)
+ val HDR10_PLUS = DynamicRangeProfile(8)
+ val DOLBY_VISION_10B_HDR_REF = DynamicRangeProfile(16)
+ val DOLBY_VISION_10B_HDR_REF_PO = DynamicRangeProfile(32)
+ val DOLBY_VISION_10B_HDR_OEM = DynamicRangeProfile(64)
+ val DOLBY_VISION_10B_HDR_OEM_PO = DynamicRangeProfile(128)
+ val DOLBY_VISION_8B_HDR_REF = DynamicRangeProfile(256)
+ val DOLBY_VISION_8B_HDR_REF_PO = DynamicRangeProfile(512)
+ val DOLBY_VISION_8B_HDR_OEM = DynamicRangeProfile(1024)
+ val DOLBY_VISION_8B_HDR_OEM_PO = DynamicRangeProfile(2048)
+ val PUBLIC_MAX = DynamicRangeProfile(4096)
+ }
+ }
+
+ /**
+ * Adds the ability to define the stream specific use case of the OutputStream.
+ * [DEFAULT] is the default stream use case, with which
+ * the camera device uses the properties of the output target, such as format, dataSpace,
+ * or surface class type, to optimize the image processing pipeline.
+ *
+ * See the documentation on [OutputConfiguration.setStreamUseCase] for more details.
+ */
+ @JvmInline
+ value class StreamUseCase(val value: Long) {
+ companion object {
+ val DEFAULT = StreamUseCase(0)
+ val PREVIEW = StreamUseCase(1)
+ val STILL_CAPTURE = StreamUseCase(2)
+ val VIDEO_RECORD = StreamUseCase(3)
+ val PREVIEW_VIDEO_STILL = StreamUseCase(4)
+ val VIDEO_CALL = StreamUseCase(5)
+ }
+ }
}
/**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 7ae56e8..8d8bec7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -308,6 +308,18 @@
internal object Api33Compat {
@JvmStatic
@DoNotInline
+ fun setDynamicRangeProfile(outputConfig: OutputConfiguration, dynamicRangeProfile: Long) {
+ outputConfig.dynamicRangeProfile = dynamicRangeProfile
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun getDynamicRangeProfile(outputConfig: OutputConfiguration): Long {
+ return outputConfig.dynamicRangeProfile
+ }
+
+ @JvmStatic
+ @DoNotInline
fun setMirrorMode(outputConfig: OutputConfiguration, mirrorMode: Int) {
outputConfig.mirrorMode = mirrorMode
}
@@ -317,6 +329,19 @@
fun getMirrorMode(outputConfig: OutputConfiguration): Int {
return outputConfig.mirrorMode
}
+
+ @JvmStatic
+ @DoNotInline
+ fun setStreamUseCase(outputConfig: OutputConfiguration, streamUseCase: Long) {
+ outputConfig.streamUseCase = streamUseCase
+ }
+
+ @JvmStatic
+ @DoNotInline
+ fun getStreamUseCase(outputConfig: OutputConfiguration): Long {
+ return outputConfig.streamUseCase
+ }
+
@JvmStatic
@DoNotInline
fun setTimestampBase(outputConfig: OutputConfiguration, timestampBase: Int) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index f781b609..6d4b953 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -325,6 +325,8 @@
outputType = outputConfig.deferredOutputType!!,
mirrorMode = outputConfig.mirrorMode,
timestampBase = outputConfig.timestampBase,
+ dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+ streamUseCase = outputConfig.streamUseCase,
surfaceSharing = outputConfig.surfaceSharing,
surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
physicalCameraId = if (outputConfig.camera != graphConfig.camera) {
@@ -354,6 +356,8 @@
outputSurfaces.first(),
mirrorMode = outputConfig.mirrorMode,
timestampBase = outputConfig.timestampBase,
+ dynamicRangeProfile = outputConfig.dynamicRangeProfile,
+ streamUseCase = outputConfig.streamUseCase,
size = outputConfig.size,
surfaceSharing = outputConfig.surfaceSharing,
surfaceGroupId = outputConfig.groupNumber ?: SURFACE_GROUP_ID_NONE,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index c7243bd..3079060 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -26,8 +26,10 @@
import android.view.SurfaceHolder
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.OutputStream.OutputType
+import androidx.camera.camera2.pipe.OutputStream.DynamicRangeProfile
import androidx.camera.camera2.pipe.OutputStream.MirrorMode
+import androidx.camera.camera2.pipe.OutputStream.OutputType
+import androidx.camera.camera2.pipe.OutputStream.StreamUseCase
import androidx.camera.camera2.pipe.OutputStream.TimestampBase
import androidx.camera.camera2.pipe.UnsafeWrapper
import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
@@ -139,6 +141,8 @@
outputType: OutputType = OutputType.SURFACE,
mirrorMode: MirrorMode? = null,
timestampBase: TimestampBase? = null,
+ dynamicRangeProfile: DynamicRangeProfile? = null,
+ streamUseCase: StreamUseCase? = null,
size: Size? = null,
surfaceSharing: Boolean = false,
surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
@@ -224,6 +228,26 @@
}
}
+ if (dynamicRangeProfile != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Api33Compat.setDynamicRangeProfile(configuration, dynamicRangeProfile.value)
+ } else {
+ if (dynamicRangeProfile != DynamicRangeProfile.STANDARD) {
+ Log.warn {
+ "Cannot set dynamicRangeProfile to a non-default value on API " +
+ "${Build.VERSION.SDK_INT}. This may result in unexpected " +
+ "behavior. Requested $dynamicRangeProfile"
+ }
+ }
+ }
+ }
+
+ if (streamUseCase != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
+ }
+ }
+
// Create and return the Android
return AndroidOutputConfiguration(
configuration,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index 3a6eee4..76f5753 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -21,6 +21,7 @@
import androidx.camera.camera2.pipe.CameraController
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.GraphState
import androidx.camera.camera2.pipe.StreamGraph
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.config.CameraGraphScope
@@ -31,6 +32,7 @@
import androidx.camera.camera2.pipe.core.acquireOrNull
import javax.inject.Inject
import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.flow.StateFlow
internal val cameraGraphIds = atomic(0)
@@ -40,6 +42,7 @@
graphConfig: CameraGraph.Config,
metadata: CameraMetadata,
private val graphProcessor: GraphProcessor,
+ private val graphListener: GraphListener,
private val streamGraph: StreamGraphImpl,
private val surfaceGraph: SurfaceGraph,
private val cameraController: CameraController,
@@ -63,9 +66,13 @@
override val streams: StreamGraph
get() = streamGraph
+ override val graphState: StateFlow<GraphState>
+ get() = graphProcessor.graphState
+
override fun start() {
Debug.traceStart { "$this#start" }
Log.info { "Starting $this" }
+ graphListener.onGraphStarting()
cameraController.start()
Debug.traceStop()
}
@@ -73,6 +80,7 @@
override fun stop() {
Debug.traceStart { "$this#stop" }
Log.info { "Stopping $this" }
+ graphListener.onGraphStopping()
cameraController.stop()
Debug.traceStop()
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
index 598657f..89ea72b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphListener.kt
@@ -17,16 +17,29 @@
package androidx.camera.camera2.pipe.graph
import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
interface GraphListener {
/**
+ * Used to indicate that the graph is starting. This is called immediately when a [CameraGraph]
+ * is being started.
+ */
+ fun onGraphStarting() {}
+
+ /**
* Used to indicate that the graph has been initialized and is ready to actively process
* requests using the provided [GraphRequestProcessor] interface.
*/
fun onGraphStarted(requestProcessor: GraphRequestProcessor)
/**
+ * Used to indicate that the graph is stopping. This is called immediately when a [CameraGraph]
+ * is being stopped.
+ */
+ fun onGraphStopping() {}
+
+ /**
* Used to indicate that a previously initialized [GraphRequestProcessor] is no longer
* available.
*/
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 598e8bf..4b23fbc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -21,6 +21,10 @@
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.GraphState
+import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
+import androidx.camera.camera2.pipe.GraphState.GraphStateStarted
import androidx.camera.camera2.pipe.CaptureSequenceProcessor
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.config.CameraGraphScope
@@ -35,6 +39,8 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
/**
* The [GraphProcessor] is responsible for queuing and then submitting them to a
@@ -42,6 +48,8 @@
* and submitted before the camera is available.
*/
internal interface GraphProcessor {
+ val graphState: StateFlow<GraphState>
+
fun submit(request: Request)
fun submit(requests: List<Request>)
suspend fun submit(parameters: Map<*, Any?>): Boolean
@@ -102,7 +110,19 @@
@GuardedBy("lock")
private var closed = false
+ private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
+
+ override val graphState: StateFlow<GraphState>
+ get() = _graphState
+
+ override fun onGraphStarting() {
+ debug { "$this onGraphStarting" }
+ _graphState.value = GraphStateStarting
+ }
+
override fun onGraphStarted(requestProcessor: GraphRequestProcessor) {
+ debug { "$this onGraphStarted" }
+ _graphState.value = GraphStateStarted
var old: GraphRequestProcessor? = null
synchronized(lock) {
if (closed) {
@@ -126,6 +146,8 @@
}
override fun onGraphStopped(requestProcessor: GraphRequestProcessor) {
+ debug { "$this onGraphStopped" }
+ _graphState.value = GraphStateStopped
var old: GraphRequestProcessor? = null
synchronized(lock) {
if (closed) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
index dc857da..d03ed3b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
@@ -121,6 +121,8 @@
},
mirrorMode = output.mirrorMode,
timestampBase = output.timestampBase,
+ dynamicRangeProfile = output.dynamicRangeProfile,
+ streamUseCase = output.streamUseCase,
externalOutputConfig =
(output as? OutputStream.Config.ExternalOutputConfig)?.output
)
@@ -143,7 +145,9 @@
outputConfig.format,
outputConfig.camera,
outputConfig.mirrorMode,
- outputConfig.timestampBase
+ outputConfig.timestampBase,
+ outputConfig.dynamicRangeProfile,
+ outputConfig.streamUseCase,
)
outputStream
}
@@ -181,6 +185,8 @@
val deferredOutputType: OutputStream.OutputType?,
val mirrorMode: OutputStream.MirrorMode?,
val timestampBase: OutputStream.TimestampBase?,
+ val dynamicRangeProfile: OutputStream.DynamicRangeProfile?,
+ val streamUseCase: OutputStream.StreamUseCase?,
) {
internal val streamBuilder = mutableListOf<CameraStream>()
val streams: List<CameraStream>
@@ -199,6 +205,8 @@
override val camera: CameraId,
override val mirrorMode: OutputStream.MirrorMode?,
override val timestampBase: OutputStream.TimestampBase?,
+ override val dynamicRangeProfile: OutputStream.DynamicRangeProfile?,
+ override val streamUseCase: OutputStream.StreamUseCase?,
) : OutputStream {
override lateinit var stream: CameraStream
override fun toString(): String = id.toString()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 23e8bd6..5b1200b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -118,6 +118,7 @@
graphConfig,
metadata,
fakeGraphProcessor,
+ fakeGraphProcessor,
streamGraph,
surfaceGraph,
cameraController,
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
index 2ceaefc..83ecdba 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/StreamGraphImplTest.kt
@@ -41,9 +41,9 @@
fun testPrecomputedTestData() {
val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
- assertThat(streamGraph.streams).hasSize(7)
- assertThat(streamGraph.streams).hasSize(7)
- assertThat(streamGraph.outputConfigs).hasSize(6)
+ assertThat(streamGraph.streams).hasSize(9)
+ assertThat(streamGraph.streams).hasSize(9)
+ assertThat(streamGraph.outputConfigs).hasSize(8)
val stream1 = streamGraph[config.streamConfig1]!!
val outputStream1 = stream1.outputs.single()
@@ -52,6 +52,8 @@
assertThat(outputStream1.size.height).isEqualTo(100)
assertThat(outputStream1.mirrorMode).isNull()
assertThat(outputStream1.timestampBase).isNull()
+ assertThat(outputStream1.dynamicRangeProfile).isNull()
+ assertThat(outputStream1.streamUseCase).isNull()
val stream2 = streamGraph[config.streamConfig2]!!
val outputStream2 = stream2.outputs.single()
@@ -61,6 +63,8 @@
assertThat(outputStream2.size.height).isEqualTo(321)
assertThat(outputStream2.mirrorMode).isNull()
assertThat(outputStream2.timestampBase).isNull()
+ assertThat(outputStream2.dynamicRangeProfile).isNull()
+ assertThat(outputStream2.streamUseCase).isNull()
}
@Test
@@ -202,4 +206,26 @@
assertThat(stream2.outputs.single().timestampBase)
.isEqualTo(OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC)
}
+
+ @Test
+ fun testDefaultAndPropagatedDynamicRangeProfiles() {
+ val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
+ val stream1 = streamGraph[config.streamConfig1]!!
+ assertThat(stream1.outputs.single().dynamicRangeProfile).isNull()
+
+ val stream2 = streamGraph[config.streamConfig6]!!
+ assertThat(stream2.outputs.single().dynamicRangeProfile)
+ .isEqualTo(OutputStream.DynamicRangeProfile.PUBLIC_MAX)
+ }
+
+ @Test
+ fun testDefaultAndPropagatedStreamUseCases() {
+ val streamGraph = StreamGraphImpl(config.fakeMetadata, config.graphConfig)
+ val stream1 = streamGraph[config.streamConfig1]!!
+ assertThat(stream1.outputs.single().streamUseCase).isNull()
+
+ val stream2 = streamGraph[config.streamConfig7]!!
+ assertThat(stream2.outputs.single().streamUseCase)
+ .isEqualTo(OutputStream.StreamUseCase.VIDEO_RECORD)
+ }
}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
index f5ed8ec..57809dd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/SurfaceGraphTest.kt
@@ -55,8 +55,10 @@
private val stream3 = streamMap[config.streamConfig3]!!
private val stream4 = streamMap[config.streamConfig4]!!
private val stream5 = streamMap[config.streamConfig5]!!
- private val stream6 = streamMap[config.sharedStreamConfig1]!!
- private val stream7 = streamMap[config.sharedStreamConfig2]!!
+ private val stream6 = streamMap[config.streamConfig6]!!
+ private val stream7 = streamMap[config.streamConfig7]!!
+ private val stream8 = streamMap[config.sharedStreamConfig1]!!
+ private val stream9 = streamMap[config.sharedStreamConfig2]!!
private val fakeSurface1 = Surface(SurfaceTexture(1))
private val fakeSurface2 = Surface(SurfaceTexture(2))
@@ -65,6 +67,8 @@
private val fakeSurface5 = Surface(SurfaceTexture(5))
private val fakeSurface6 = Surface(SurfaceTexture(6))
private val fakeSurface7 = Surface(SurfaceTexture(7))
+ private val fakeSurface8 = Surface(SurfaceTexture(8))
+ private val fakeSurface9 = Surface(SurfaceTexture(9))
@After
fun teardown() {
@@ -75,6 +79,8 @@
fakeSurface5.release()
fakeSurface6.release()
fakeSurface7.release()
+ fakeSurface8.release()
+ fakeSurface9.release()
}
@Test
@@ -86,6 +92,8 @@
surfaceGraph[stream5.id] = fakeSurface5
surfaceGraph[stream6.id] = fakeSurface6
surfaceGraph[stream7.id] = fakeSurface7
+ surfaceGraph[stream8.id] = fakeSurface8
+ surfaceGraph[stream9.id] = fakeSurface9
assertThat(controller.surfaceMap).isNotNull()
assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1)
@@ -93,6 +101,8 @@
assertThat(controller.surfaceMap?.get(stream3.id)).isEqualTo(fakeSurface3)
assertThat(controller.surfaceMap?.get(stream4.id)).isEqualTo(fakeSurface4)
assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
+ assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
+ assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
}
@Test
@@ -104,10 +114,12 @@
surfaceGraph[stream3.id] = fakeSurface3
surfaceGraph[stream4.id] = fakeSurface4
surfaceGraph[stream5.id] = fakeSurface5
- assertThat(controller.surfaceMap).isNull()
-
surfaceGraph[stream6.id] = fakeSurface6
surfaceGraph[stream7.id] = fakeSurface7
+ assertThat(controller.surfaceMap).isNull()
+
+ surfaceGraph[stream8.id] = fakeSurface8
+ surfaceGraph[stream9.id] = fakeSurface9
assertThat(controller.surfaceMap).isNotNull()
assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1)
@@ -117,6 +129,8 @@
assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
+ assertThat(controller.surfaceMap?.get(stream8.id)).isEqualTo(fakeSurface8)
+ assertThat(controller.surfaceMap?.get(stream9.id)).isEqualTo(fakeSurface9)
}
@Test
@@ -134,6 +148,8 @@
surfaceGraph[stream5.id] = fakeSurface5
surfaceGraph[stream6.id] = fakeSurface6
surfaceGraph[stream7.id] = fakeSurface7
+ surfaceGraph[stream8.id] = fakeSurface8
+ surfaceGraph[stream9.id] = fakeSurface9
assertThat(controller.surfaceMap).isNotNull()
assertThat(controller.surfaceMap?.get(stream1.id)).isEqualTo(fakeSurface1B)
@@ -143,6 +159,8 @@
assertThat(controller.surfaceMap?.get(stream5.id)).isEqualTo(fakeSurface5)
assertThat(controller.surfaceMap?.get(stream6.id)).isEqualTo(fakeSurface6)
assertThat(controller.surfaceMap?.get(stream7.id)).isEqualTo(fakeSurface7)
+ assertThat(controller.surfaceMap?.get(stream8.id)).isEqualTo(fakeSurface8)
+ assertThat(controller.surfaceMap?.get(stream9.id)).isEqualTo(fakeSurface9)
fakeSurface1A.release()
fakeSurface1B.release()
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
index fce96de..862313f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphConfigs.kt
@@ -88,6 +88,27 @@
mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_MONOTONIC
)
+ val streamConfig6 = CameraStream.Config.create(
+ size = Size(200, 200),
+ format = StreamFormat.YUV_420_888,
+ camera = camera2,
+ outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+ mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+ timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+ dynamicRangeProfile = OutputStream.DynamicRangeProfile.PUBLIC_MAX
+ )
+
+ val streamConfig7 = CameraStream.Config.create(
+ size = Size(200, 200),
+ format = StreamFormat.YUV_420_888,
+ camera = camera2,
+ outputType = OutputStream.OutputType.SURFACE_TEXTURE,
+ mirrorMode = OutputStream.MirrorMode.MIRROR_MODE_AUTO,
+ timestampBase = OutputStream.TimestampBase.TIMESTAMP_BASE_DEFAULT,
+ dynamicRangeProfile = OutputStream.DynamicRangeProfile.STANDARD,
+ streamUseCase = OutputStream.StreamUseCase.VIDEO_RECORD
+ )
+
val sharedOutputConfig = OutputStream.Config.create(
size = Size(200, 200),
format = StreamFormat.YUV_420_888,
@@ -104,6 +125,8 @@
streamConfig3,
streamConfig4,
streamConfig5,
+ streamConfig6,
+ streamConfig7,
sharedStreamConfig1,
sharedStreamConfig2
),
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 1c9b2d0..725316b 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -16,12 +16,15 @@
package androidx.camera.camera2.pipe.testing
+import androidx.camera.camera2.pipe.GraphState
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.graph.GraphListener
import androidx.camera.camera2.pipe.graph.GraphProcessor
import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
import androidx.camera.camera2.pipe.graph.GraphState3A
import androidx.camera.camera2.pipe.putAllMetadata
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
/**
* Fake implementation of a [GraphProcessor] for tests.
@@ -43,6 +46,11 @@
private val _requestQueue = mutableListOf<List<Request>>()
private var processor: GraphRequestProcessor? = null
+ private val _graphState = MutableStateFlow<GraphState>(GraphState.GraphStateStopped)
+
+ override val graphState: StateFlow<GraphState>
+ get() = _graphState
+
override fun startRepeating(request: Request) {
repeatingRequest = request
}
@@ -58,6 +66,7 @@
override fun submit(requests: List<Request>) {
_requestQueue.add(requests)
}
+
override suspend fun submit(parameters: Map<*, Any?>): Boolean {
if (closed) {
return false
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 325da4f..6ebf92f 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -69,6 +69,9 @@
import androidx.camera.testing.fakes.FakeCameraInfoInternal;
import androidx.camera.testing.fakes.FakeUseCase;
import androidx.camera.testing.fakes.FakeUseCaseConfig;
+import androidx.camera.testing.mocks.MockObserver;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast;
import androidx.core.os.HandlerCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -91,12 +94,9 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Semaphore;
@@ -534,10 +534,11 @@
@Test
public void cameraTransitionsThroughPendingState_whenNoCamerasAvailable() {
- CameraInternalStateMockObserver mockObserver = new CameraInternalStateMockObserver();
+ MockObserver<CameraInternal.State> mockObserver = new MockObserver<>();
// Ensure real camera can't open due to max cameras being open
Camera mockCamera = mock(Camera.class);
+
mCameraStateRegistry.registerCamera(mockCamera, CameraXExecutors.directExecutor(),
() -> {
});
@@ -550,7 +551,7 @@
// Ensure that the camera gets to a PENDING_OPEN state
mockObserver.verifyOnNewDataCall(CameraInternal.State.PENDING_OPEN, 3000,
- times -> times > 0);
+ new CallTimesAtLeast(1));
// Allow camera to be opened
mCameraStateRegistry.markCameraState(mockCamera, CameraInternal.State.CLOSED);
@@ -593,7 +594,7 @@
// Wait for the secondary capture session is configured.
assertTrue(mSessionStateCallback.waitForOnConfigured(1));
- CameraInternalStateMockObserver mockObserver = new CameraInternalStateMockObserver();
+ MockObserver<CameraInternal.State> mockObserver = new MockObserver<>();
mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
mockObserver);
@@ -602,7 +603,8 @@
// Wait for the CLOSED state. If the test fail, the CameraX might in wrong internal state,
// and the Camera2CameraImpl#release() might stuck.
- mockObserver.verifyOnNewDataCall(CameraInternal.State.CLOSED, 4000, times -> times == 1);
+ mockObserver.verifyOnNewDataCall(CameraInternal.State.CLOSED, 4000,
+ new CallTimes(1));
}
@Test
@@ -1044,64 +1046,4 @@
return suggestedResolution;
}
}
-
- static class CameraInternalStateMockObserver
- implements Observable.Observer<CameraInternal.State> {
- public interface CallTimesCondition {
- boolean check(int times);
- }
-
- private final Map<CameraInternal.State, Integer> mNewDataCount = new HashMap<>();
-
- private CallTimesCondition mCallTimesCondition;
- private CameraInternal.State mValueToVerify;
- private CountDownLatch mLatch;
-
- private boolean isVerified() {
- int count = mNewDataCount.get(mValueToVerify) == null
- ? 0 : mNewDataCount.get(mValueToVerify);
- return mCallTimesCondition.check(count);
- }
-
- public void verifyOnNewDataCall(@Nullable CameraInternal.State value,
- long timeoutInMillis) {
- verifyOnNewDataCall(value, timeoutInMillis, times -> times == 1);
- }
-
- public void verifyOnNewDataCall(@Nullable CameraInternal.State value,
- long timeoutInMillis, CallTimesCondition callTimesCondition) {
- mValueToVerify = value;
- mCallTimesCondition = callTimesCondition;
-
- if (!isVerified()) {
- mLatch = new CountDownLatch(1);
-
- try {
- assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
- mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
-
- assertTrue("onNewData called "
- + mNewDataCount.get(value) + " time(s) with " + value,
- isVerified());
-
- mLatch = null;
- }
- }
-
- @Override
- public void onNewData(@Nullable CameraInternal.State value) {
- Integer prevCount = mNewDataCount.get(value);
- mNewDataCount.put(value, (prevCount == null ? 0 : prevCount) + 1);
-
- if (mLatch != null && isVerified()) {
- mLatch.countDown();
- }
- }
-
- @Override
- public void onError(@NonNull Throwable t) {}
- };
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
index 748e0c5..8be1ac2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/DisplayInfoManager.java
@@ -32,6 +32,8 @@
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class DisplayInfoManager {
+
+ private static final String TAG = "DisplayInfoManager";
private static final Size MAX_PREVIEW_SIZE = new Size(1920, 1080);
private static final Object INSTANCE_LOCK = new Object();
private static volatile DisplayInfoManager sInstance;
@@ -85,24 +87,41 @@
return displays[0];
}
- Display maxDisplay = null;
- int maxDisplaySize = -1;
+ Display maxDisplayWhenStateOn = null;
+ int maxDisplaySizeWhenStateOn = -1;
for (Display display : displays) {
if (display.getState() != Display.STATE_OFF) {
Point displaySize = new Point();
display.getRealSize(displaySize);
+ if (displaySize.x * displaySize.y > maxDisplaySizeWhenStateOn) {
+ maxDisplaySizeWhenStateOn = displaySize.x * displaySize.y;
+ maxDisplayWhenStateOn = display;
+ }
+ }
+ }
+
+ if (maxDisplayWhenStateOn == null) {
+ // If there is no display found with state on, search display with state off as well.
+ // If still no display found, throw IllegalArgumentException.
+ Display maxDisplay = null;
+ int maxDisplaySize = -1;
+ for (Display display : displays) {
+ Point displaySize = new Point();
+ display.getRealSize(displaySize);
if (displaySize.x * displaySize.y > maxDisplaySize) {
maxDisplaySize = displaySize.x * displaySize.y;
maxDisplay = display;
}
}
- }
- if (maxDisplay == null) {
- throw new IllegalArgumentException("No display can be found from the input display "
- + "manager!");
+ if (maxDisplay == null) {
+ throw new IllegalArgumentException("No display can be found from the input display "
+ + "manager!");
+ }
+
+ return maxDisplay;
}
- return maxDisplay;
+ return maxDisplayWhenStateOn;
}
/**
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
index fafca66..8fff586 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/DisplayInfoManagerTest.kt
@@ -34,8 +34,9 @@
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowDisplayManager
import org.robolectric.shadows.ShadowDisplay
+import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@@ -109,6 +110,36 @@
assertThat(size).isEqualTo(Point(480, 640))
}
+ @Test(expected = IllegalArgumentException::class)
+ fun canReturnMaxSizeDisplay_noDisplay() {
+ // Arrange
+ removeDisplay(0)
+
+ // Act
+ val displayInfoManager = DisplayInfoManager
+ .getInstance(ApplicationProvider.getApplicationContext())
+ val size = Point()
+ displayInfoManager.maxSizeDisplay.getRealSize(size)
+ }
+
+ @Test
+ fun canReturnMaxSizeDisplay_allOffState() {
+ // Arrange
+ addDisplay(480, 640, Display.STATE_OFF)
+ addDisplay(200, 300, Display.STATE_OFF)
+ addDisplay(2000, 3000, Display.STATE_OFF)
+ removeDisplay(0)
+
+ // Act
+ val displayInfoManager = DisplayInfoManager
+ .getInstance(ApplicationProvider.getApplicationContext())
+ val size = Point()
+ displayInfoManager.maxSizeDisplay.getRealSize(size)
+
+ // Assert
+ assertThat(size).isEqualTo(Point(2000, 3000))
+ }
+
@Test
fun canReturnPreviewSize_displaySmallerThan1080P() {
// Arrange
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
index 6ff346a..24f4e72 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
@@ -171,6 +171,23 @@
}
@Test
+ fun handleInvalidate_runWhenInvalidateCalled() {
+ // Arrange.
+ var isCalled = false
+ val request = createNewRequest(FAKE_SIZE) {
+ isCalled = true
+ }
+ Truth.assertThat(isCalled).isFalse()
+
+ // Act.
+ request.willNotProvideSurface()
+ request.invalidate()
+
+ // Assert.
+ Truth.assertThat(isCalled).isTrue()
+ }
+
+ @Test
@Suppress("UNCHECKED_CAST")
fun cancelledRequest_resultsInREQUEST_CANCELLED() {
val request = createNewRequest(FAKE_SIZE)
@@ -331,9 +348,10 @@
private fun createNewRequest(
size: Size,
expectedFrameRate: Range<Int>? = null,
- autoCleanup: Boolean = true
+ autoCleanup: Boolean = true,
+ onInvalidated: () -> Unit = {},
): SurfaceRequest {
- val request = SurfaceRequest(size, FakeCamera(), false, expectedFrameRate)
+ val request = SurfaceRequest(size, FakeCamera(), false, expectedFrameRate, onInvalidated)
if (autoCleanup) {
surfaceRequests.add(request)
}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
index 76601d7..706d6ec 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallback.kt
@@ -28,19 +28,33 @@
*/
class FakeTakePictureCallback : TakePictureCallback {
- private lateinit var inMemoryResultCont: Continuation<ImageProxy>
- private lateinit var onDiskResultCont: Continuation<OutputFileResults>
+ private var inMemoryResult: ImageProxy? = null
+ private var inMemoryResultCont: Continuation<ImageProxy>? = null
+ private var onDiskResult: OutputFileResults? = null
+ private var onDiskResultCont: Continuation<OutputFileResults>? = null
override fun onImageCaptured() {
TODO("Not yet implemented")
}
override fun onFinalResult(outputFileResults: OutputFileResults) {
- onDiskResultCont.resume(outputFileResults)
+ val cont = onDiskResultCont
+ if (cont != null) {
+ cont.resume(outputFileResults)
+ >
+ } else {
+ >
+ }
}
override fun onFinalResult(imageProxy: ImageProxy) {
- inMemoryResultCont.resume(imageProxy)
+ val cont = inMemoryResultCont
+ if (cont != null) {
+ cont.resume(imageProxy)
+ inMemoryResultCont = null
+ } else {
+ inMemoryResult = imageProxy
+ }
}
override fun onCaptureFailure(imageCaptureException: ImageCaptureException) {
@@ -56,10 +70,18 @@
}
internal suspend fun getInMemoryResult() = suspendCoroutine { cont ->
- inMemoryResultCont = cont
+ if (inMemoryResult != null) {
+ cont.resume(inMemoryResult!!)
+ } else {
+ inMemoryResultCont = cont
+ }
}
internal suspend fun getOnDiskResult() = suspendCoroutine { cont ->
- >
+ if (onDiskResult != null) {
+ cont.resume(onDiskResult!!)
+ } else {
+ >
+ }
}
}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt
new file mode 100644
index 0000000..e7cb20b
--- /dev/null
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/FakeTakePictureCallbackDeviceTest.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture
+
+import androidx.annotation.RequiresApi
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.testing.fakes.FakeImageInfo
+import androidx.camera.testing.fakes.FakeImageProxy
+import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+
+/**
+ * Unit tests for [FakeTakePictureCallbackDeviceTest]
+ */
+@RequiresApi(21)
+@SdkSuppress(minSdkVersion = 21)
+class FakeTakePictureCallbackDeviceTest {
+
+ private val fakeTakePictureCallback = FakeTakePictureCallback()
+
+ @Test
+ fun onDiskResultArrivesBeforeGet_canGetResult() = runBlocking {
+ // Arrange.
+ val >
+ // Assert.
+ fakeTakePictureCallback.onFinalResult(onDiskResult)
+ // Act.
+ assertThat(fakeTakePictureCallback.getOnDiskResult()).isEqualTo(onDiskResult)
+ }
+
+ @Test
+ fun inMemoryResultArrivesBeforeGet_canGetResult() = runBlocking {
+ // Arrange.
+ val inMemoryResult = FakeImageProxy(FakeImageInfo())
+ // Assert.
+ fakeTakePictureCallback.onFinalResult(inMemoryResult)
+ // Act.
+ assertThat(fakeTakePictureCallback.getInMemoryResult()).isEqualTo(inMemoryResult)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
index 05fdc22..3f88ffa 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
@@ -304,7 +304,7 @@
}
private fun createInputSurfaceRequest(): SurfaceRequest {
- return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera, false).apply {
+ return SurfaceRequest(Size(WIDTH, HEIGHT), fakeCamera, false) {}.apply {
inputSurfaceRequestsToClose.add(this)
}
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index a13c3ca..a689360 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -279,7 +279,7 @@
@FlashMode
private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
- boolean mUseProcessingPipeline = false;
+ boolean mUseProcessingPipeline = true;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
static final ExifRotationAvailability EXIF_ROTATION_AVAILABILITY =
@@ -1939,10 +1939,15 @@
checkMainThread();
Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
cameraId, resolution));
+
checkState(mImagePipeline == null);
mImagePipeline = new ImagePipeline(config, resolution, mCameraEffect);
- checkState(mTakePictureManager == null);
- mTakePictureManager = new TakePictureManager(mImageCaptureControl, mImagePipeline);
+
+ if (mTakePictureManager == null) {
+ // mTakePictureManager is reused when the Surface is reset.
+ mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+ }
+ mTakePictureManager.setImagePipeline(mImagePipeline);
SessionConfig.Builder sessionConfigBuilder = mImagePipeline.createSessionConfigBuilder();
if (Build.VERSION.SDK_INT >= 23 && getCaptureMode() == CAPTURE_MODE_ZERO_SHUTTER_LAG) {
@@ -1953,7 +1958,8 @@
// to this use case so we don't need to do this check.
if (isCurrentCamera(cameraId)) {
mTakePictureManager.pause();
- // TODO: we might need to ask mImagePipeline to recreate camera Surface.
+ clearPipelineWithNode(/*keepTakePictureManager=*/ true);
+ mSessionConfigBuilder = createPipeline(cameraId, config, resolution);
updateSessionConfig(mSessionConfigBuilder.build());
notifyReset();
mTakePictureManager.resume();
@@ -2028,6 +2034,15 @@
return new Rect(0, 0, resolution.getWidth(), resolution.getHeight());
}
+
+ /**
+ * Clears the pipeline without keeping the {@link TakePictureManager}.
+ */
+ @MainThread
+ private void clearPipelineWithNode() {
+ clearPipelineWithNode(/*keepTakePictureManager=*/false);
+ }
+
/**
* Clears the pipeline.
*
@@ -2035,13 +2050,15 @@
* resources.
*/
@MainThread
- private void clearPipelineWithNode() {
+ private void clearPipelineWithNode(boolean keepTakePictureManager) {
Log.d(TAG, "clearPipelineWithNode");
checkMainThread();
mImagePipeline.close();
mImagePipeline = null;
- mTakePictureManager.abortRequests();
- mTakePictureManager = null;
+ if (!keepTakePictureManager) {
+ mTakePictureManager.abortRequests();
+ mTakePictureManager = null;
+ }
}
/**
@@ -2082,6 +2099,12 @@
return mCameraEffect;
}
+ @VisibleForTesting
+ @NonNull
+ TakePictureManager getTakePictureManager() {
+ return mTakePictureManager;
+ }
+
// ===== New architecture end =====
/**
@@ -3081,7 +3104,8 @@
* will be finally selected will depend on the camera device's hardware level and the
* bound use cases combination. For more details see the guaranteed supported
* configurations tables in {@link android.hardware.camera2.CameraDevice}'s
- * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a> section.
+ * href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice
+ * #regular-capture">Regular capture</a> section.
*
* @param resolution The target resolution to choose from supported output sizes list.
* @return The current Builder.
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 dab5cfd..8eff18d 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
@@ -228,8 +228,12 @@
clearPipeline();
boolean isRGBA8888SurfaceRequired = config.isRgba8888SurfaceRequired(false);
+
+ // TODO: Can be improved by only restarting part of the pipeline when using the
+ // CaptureProcessor. e.g. only update the output Surface (between Processor/App), and
+ // still use the same input Surface (between Camera/Processor). It's just simpler for now.
final SurfaceRequest surfaceRequest = new SurfaceRequest(resolution, getCamera(),
- isRGBA8888SurfaceRequired);
+ isRGBA8888SurfaceRequired, this::notifyReset);
mCurrentSurfaceRequest = surfaceRequest;
if (mSurfaceProvider != null) {
@@ -319,7 +323,8 @@
/*hasEmbeddedTransform=*/true,
requireNonNull(getCropRect(resolution)),
getRelativeRotation(camera),
- /*mirroring=*/false);
+ /*mirroring=*/false,
+ this::notifyReset);
SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
SurfaceEdge outputEdge = mNode.transform(inputEdge);
SettableSurface appSurface = outputEdge.getSurfaces().get(0);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
index 159949c..f7bdce3 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SafeCloseImageReaderProxy.java
@@ -23,6 +23,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
import androidx.camera.core.impl.ImageReaderProxy;
@@ -140,6 +141,16 @@
}
/**
+ * Check if the {@link SafeCloseImageReaderProxy} is closed for testing.
+ */
+ @VisibleForTesting
+ public boolean isClosed() {
+ synchronized (mLock) {
+ return mIsClosed;
+ }
+ }
+
+ /**
* Returns the number of empty slots in the queue.
*/
public int getCapacity() {
@@ -150,6 +161,7 @@
/**
* Sets a listener for close calls on this image.
+ *
* @param listener to set
*/
public void setOnImageCloseListener(@NonNull OnImageCloseListener listener) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
index 374f3a2..523ad9d 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
@@ -84,6 +84,10 @@
// from the camera.
private final ListenableFuture<Void> mSessionStatusFuture;
+ // For notification of surface recreated.
+ @NonNull
+ private final CallbackToFutureAdapter.Completer<Void> mSurfaceRecreationCompleter;
+
// For notification of surface request cancellation. Should only be used to register
// cancellation listeners.
private final CallbackToFutureAdapter.Completer<Void> mRequestCancellationCompleter;
@@ -110,13 +114,15 @@
public SurfaceRequest(
@NonNull Size resolution,
@NonNull CameraInternal camera,
- boolean isRGBA8888Required) {
- this(resolution, camera, isRGBA8888Required, /*expectedFrameRate=*/null);
+ boolean isRGBA8888Required,
+ @NonNull Runnable onInvalidated) {
+ this(resolution, camera, isRGBA8888Required, /*expectedFrameRate=*/null, onInvalidated);
}
/**
* Creates a new surface request with the given resolution, {@link Camera}, and an optional
* expected frame rate.
+ *
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -124,7 +130,8 @@
@NonNull Size resolution,
@NonNull CameraInternal camera,
boolean isRGBA8888Required,
- @Nullable Range<Integer> expectedFrameRate) {
+ @Nullable Range<Integer> expectedFrameRate,
+ @NonNull Runnable onInvalidated) {
super();
mResolution = resolution;
mCamera = camera;
@@ -244,6 +251,9 @@
// finished with the surface, so cancelling the surface future below will be a no-op.
terminationFuture.addListener(() -> mSurfaceFuture.cancel(true),
CameraXExecutors.directExecutor());
+
+ mSurfaceRecreationCompleter = initialSurfaceRecreationCompleter(
+ CameraXExecutors.directExecutor(), onInvalidated);
}
/**
@@ -420,6 +430,72 @@
}
/**
+ * Sets a {@link Runnable} that handles the situation where {@link Surface} is no longer valid
+ * and triggers the process to request a new {@link Surface}.
+ *
+ * @param executor Executor used to execute the {@code runnable}.
+ * @param runnable The code which will be run when {@link Surface} is no longer valid.
+ */
+ private CallbackToFutureAdapter.Completer<Void> initialSurfaceRecreationCompleter(
+ @NonNull Executor executor, @NonNull Runnable runnable) {
+ AtomicReference<CallbackToFutureAdapter.Completer<Void>> completerRef =
+ new AtomicReference<>(null);
+ final ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+ completerRef.set(completer);
+ return "SurfaceRequest-surface-recreation(" + SurfaceRequest.this.hashCode() + ")";
+ });
+
+ Futures.addCallback(future, new FutureCallback<Void>() {
+ @Override
+ public void onSuccess(@Nullable Void result) {
+ runnable.run();
+ }
+
+ @Override
+ public void onFailure(@NonNull Throwable t) {
+ // Do nothing
+ }
+ }, executor);
+
+ return Preconditions.checkNotNull(completerRef.get());
+ }
+
+ /**
+ * Invalidate the previously provided {@link Surface} to provide a new {@link Surface}.
+ *
+ * <p>Call this method to inform the surface requester that the previously provided
+ * {@link Surface} is no longer valid (e.g. when the SurfaceView destroys its surface due to
+ * page being slid out in ViewPager2) and should re-send a {@link SurfaceRequest} to obtain a
+ * new {@link Surface}.
+ *
+ * <p>Calling this method will cause the camera to be reconfigured. The app should call this
+ * method when the surface provider is ready to provide a new {@link Surface}. (e.g. a
+ * SurfaceView's surface is created when its window is visible.)
+ *
+ * <p>If the provided {@link Surface} was already invalidated, invoking this method will return
+ * {@code false}, and will have no effect. The surface requester will not be notified again, so
+ * there will not be another {@link SurfaceRequest}.
+ *
+ * <p>Calling this method without {@link #provideSurface(Surface, Executor, Consumer)}
+ * (regardless of whether @link #willNotProvideSurface()} has been called) will still trigger
+ * the surface requester to re-send a {@link SurfaceRequest}.
+ *
+ * <p>Since calling this method also means that the {@link SurfaceRequest} will not be
+ * fulfilled, if the {@link SurfaceRequest} is not completed, it will be completed as if
+ * calling {@link #willNotProvideSurface()}.
+ *
+ * @return true if the provided {@link Surface} is invalidated or false if it was already
+ * invalidated.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public boolean invalidate() {
+ willNotProvideSurface();
+ return mSurfaceRecreationCompleter.set(null);
+ }
+
+ /**
* Adds a listener to be informed when the camera cancels the surface request.
*
* <p>A surface request may be cancelled by the camera if the surface is no longer required.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
index 3199bcb..07e2fd7 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/CaptureNode.java
@@ -29,6 +29,7 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ForwardingImageProxy;
@@ -56,7 +57,6 @@
*
* <p>It's also responsible for managing the {@link ImageReaderProxy}. It makes sure that the
* queue is not overflowed.
- *
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
class CaptureNode implements Node<CaptureNode.In, CaptureNode.Out> {
@@ -72,13 +72,18 @@
private final Set<ImageProxy> mPendingImages = new HashSet<>();
private ProcessingRequest mCurrentRequest = null;
+ @Nullable
SafeCloseImageReaderProxy mSafeCloseImageReaderProxy;
+ @Nullable
private Out mOutputEdge;
+ @Nullable
private In mInputEdge;
@NonNull
@Override
public Out transform(@NonNull In inputEdge) {
+ checkState(mInputEdge == null && mSafeCloseImageReaderProxy == null,
+ "CaptureNode does not support recreation yet.");
mInputEdge = inputEdge;
Size size = inputEdge.getSize();
int format = inputEdge.getFormat();
@@ -127,7 +132,7 @@
}
// Send the image downstream.
- mOutputEdge.getImageEdge().accept(imageProxy);
+ requireNonNull(mOutputEdge).getImageEdge().accept(imageProxy);
}
@VisibleForTesting
@@ -146,7 +151,7 @@
mPendingStageIds.addAll(request.getStageIds());
// Send the request downstream.
- mOutputEdge.getRequestEdge().accept(request);
+ requireNonNull(mOutputEdge).getRequestEdge().accept(request);
// Match pending images and send them downstream.
for (ImageProxy imageProxy : mPendingImages) {
@@ -159,18 +164,23 @@
@Override
public void release() {
checkMainThread();
- if (mSafeCloseImageReaderProxy != null) {
- mSafeCloseImageReaderProxy.safeClose();
- }
- if (mInputEdge != null) {
- mInputEdge.closeSurface();
- }
+ releaseInputResources(requireNonNull(mInputEdge),
+ requireNonNull(mSafeCloseImageReaderProxy));
+ }
+
+ private void releaseInputResources(@NonNull CaptureNode.In inputEdge,
+ @NonNull SafeCloseImageReaderProxy imageReader) {
+ inputEdge.getSurface().close();
+ // Wait for the termination to close the ImageReader or the Surface may be released
+ // prematurely before it can be used by camera2.
+ inputEdge.getSurface().getTerminationFuture().addListener(
+ imageReader::safeClose, mainThreadExecutor());
}
@VisibleForTesting
@NonNull
In getInputEdge() {
- return mInputEdge;
+ return requireNonNull(mInputEdge);
}
@MainThread
@@ -230,10 +240,6 @@
mSurface = new ImmediateSurface(surface);
}
- void closeSurface() {
- mSurface.close();
- }
-
/**
* Edge that accepts image metadata.
*
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
index e9af56a..c9816f5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RequestWithCallback.java
@@ -42,21 +42,29 @@
class RequestWithCallback implements TakePictureCallback {
private final TakePictureRequest mTakePictureRequest;
+ private final TakePictureRequest.RetryControl mRetryControl;
private final ListenableFuture<Void> mCaptureFuture;
+ private final ListenableFuture<Void> mCompleteFuture;
private CallbackToFutureAdapter.Completer<Void> mCaptureCompleter;
- // Tombstone flag that indicates that this callback should not be invoked anymore.
- private boolean mIsComplete = false;
+ private CallbackToFutureAdapter.Completer<Void> mCompleteCompleter;
// Flag tracks if the request has been aborted by the UseCase. Once aborted, this class stops
// propagating callbacks to the app.
private boolean mIsAborted = false;
- RequestWithCallback(@NonNull TakePictureRequest takePictureRequest) {
+ RequestWithCallback(@NonNull TakePictureRequest takePictureRequest,
+ @NonNull TakePictureRequest.RetryControl retryControl) {
mTakePictureRequest = takePictureRequest;
+ mRetryControl = retryControl;
mCaptureFuture = CallbackToFutureAdapter.getFuture(
completer -> {
mCaptureCompleter = completer;
return "CaptureCompleteFuture";
});
+ mCompleteFuture = CallbackToFutureAdapter.getFuture(
+ completer -> {
+ mCompleteCompleter = completer;
+ return "RequestCompleteFuture";
+ });
}
@MainThread
@@ -98,7 +106,6 @@
mTakePictureRequest.onResult(imageProxy);
}
-
@MainThread
@Override
public void onProcessFailure(@NonNull ImageCaptureException imageCaptureException) {
@@ -125,19 +132,35 @@
// Fail silently if the request has been aborted.
return;
}
+ if (mTakePictureRequest.decrementRetryCounter()) {
+ mRetryControl.retryRequest(mTakePictureRequest);
+ } else {
+ onFailure(imageCaptureException);
+ }
markComplete();
mCaptureCompleter.set(null);
+ }
- // TODO(b/242683221): Add retry logic.
+ @MainThread
+ void abortAndSendErrorToApp(@NonNull ImageCaptureException imageCaptureException) {
+ checkMainThread();
+ abort();
onFailure(imageCaptureException);
}
@MainThread
- void abort(@NonNull ImageCaptureException imageCaptureException) {
+ void abortSilentlyAndRetry() {
+ checkMainThread();
+ abort();
+ mRetryControl.retryRequest(mTakePictureRequest);
+ }
+
+ @MainThread
+ private void abort() {
checkMainThread();
mIsAborted = true;
mCaptureCompleter.set(null);
- onFailure(imageCaptureException);
+ mCompleteCompleter.set(null);
}
/**
@@ -152,14 +175,26 @@
return mCaptureFuture;
}
+ /**
+ * Gets a {@link ListenableFuture} that finishes when the capture is completed.
+ *
+ * <p>A request is completed when it gets either a result or an unrecoverable error.
+ */
+ @MainThread
+ @NonNull
+ ListenableFuture<Void> getCompleteFuture() {
+ checkMainThread();
+ return mCompleteFuture;
+ }
+
private void checkOnImageCaptured() {
checkState(mCaptureFuture.isDone(),
"onImageCaptured() must be called before onFinalResult()");
}
private void markComplete() {
- checkState(!mIsComplete, "The callback can only complete once.");
- mIsComplete = true;
+ checkState(!mCompleteFuture.isDone(), "The callback can only complete once.");
+ mCompleteCompleter.set(null);
}
@MainThread
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index 2ecfcd5..49f346b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -44,7 +44,9 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Deque;
+import java.util.List;
/**
* Manages {@link ImageCapture#takePicture} calls.
@@ -61,34 +63,43 @@
* <p>The thread safety is guaranteed by using the main thread.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class TakePictureManager implements OnImageCloseListener {
+public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
private static final String TAG = "TakePictureManager";
// Queue of new requests that have not been sent to the pipeline/camera.
@VisibleForTesting
final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
- final ImagePipeline mImagePipeline;
final ImageCaptureControl mImageCaptureControl;
+ ImagePipeline mImagePipeline;
- // The current request being processed by camera. Null if the camera is idle.
- @VisibleForTesting
+ // The current request being processed by the camera. Only one request can be processed by
+ // the camera at the same time. Null if the camera is idle.
@Nullable
- RequestWithCallback mInFlightRequest;
+ private RequestWithCallback mCapturingRequest;
+ // The current requests that have not received a result or an error.
+ private final List<RequestWithCallback> mIncompleteRequests;
// Once paused, the class waits until the class is resumed to handle new requests.
boolean mPaused = false;
/**
* @param imageCaptureControl for controlling {@link ImageCapture}
- * @param imagePipeline for building capture requests and post-processing camera output.
*/
@MainThread
- public TakePictureManager(
- @NonNull ImageCaptureControl imageCaptureControl,
- @NonNull ImagePipeline imagePipeline) {
+ public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
checkMainThread();
mImageCaptureControl = imageCaptureControl;
+ mIncompleteRequests = new ArrayList<>();
+ }
+
+ /**
+ * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+ * output.
+ */
+ @MainThread
+ public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+ checkMainThread();
mImagePipeline = imagePipeline;
mImagePipeline.setOnImageCloseListener(this);
}
@@ -105,6 +116,14 @@
issueNextRequest();
}
+ @MainThread
+ @Override
+ public void retryRequest(@NonNull TakePictureRequest request) {
+ checkMainThread();
+ // Insert the request to the front of the queue.
+ mNewRequests.addFirst(request);
+ }
+
/**
* Pauses sending request to camera.
*/
@@ -112,8 +131,11 @@
public void pause() {
checkMainThread();
mPaused = true;
- // TODO(b/242683221): increment the retry counter on the in-flight request. The
- // mInFlightRequest may fail due to the pausing and need one more retry.
+
+ // Always retry because the camera may not send an error callback during the reset.
+ if (mCapturingRequest != null) {
+ mCapturingRequest.abortSilentlyAndRetry();
+ }
}
/**
@@ -142,9 +164,11 @@
mNewRequests.clear();
// Abort the in-flight request after clearing the pending requests.
- if (mInFlightRequest != null) {
- // TODO: optimize the performance by aborting early in CaptureNode and BundlingNode
- mInFlightRequest.abort(exception);
+ // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+ List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+ for (RequestWithCallback request : requestsSnapshot) {
+ // TODO: optimize the performance by not processing aborted requests.
+ request.abortAndSendErrorToApp(exception);
}
}
@@ -155,7 +179,7 @@
void issueNextRequest() {
checkMainThread();
Log.d(TAG, "Issue the next TakePictureRequest.");
- if (hasInFlightRequest()) {
+ if (hasCapturingRequest()) {
Log.d(TAG, "There is already a request in-flight.");
return;
}
@@ -173,8 +197,8 @@
return;
}
- RequestWithCallback requestWithCallback = new RequestWithCallback(request);
- trackCurrentRequest(requestWithCallback);
+ RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+ trackCurrentRequests(requestWithCallback);
// Send requests.
Pair<CameraRequest, ProcessingRequest> requests =
@@ -187,13 +211,21 @@
/**
* Waits for the request to finish before issuing the next.
*/
- private void trackCurrentRequest(@NonNull RequestWithCallback requestWithCallback) {
- checkState(!hasInFlightRequest());
- mInFlightRequest = requestWithCallback;
- mInFlightRequest.getCaptureFuture().addListener(() -> {
- mInFlightRequest = null;
+ private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+ checkState(!hasCapturingRequest());
+ mCapturingRequest = requestWithCallback;
+
+ // Waits for the capture to finish before issuing the next.
+ mCapturingRequest.getCaptureFuture().addListener(() -> {
+ mCapturingRequest = null;
issueNextRequest();
}, directExecutor());
+
+ // Track all incomplete requests so we can abort them when UseCase is detached.
+ mIncompleteRequests.add(requestWithCallback);
+ requestWithCallback.getCompleteFuture().addListener(() -> {
+ mIncompleteRequests.remove(requestWithCallback);
+ }, directExecutor());
}
/**
@@ -232,8 +264,19 @@
}
@VisibleForTesting
- boolean hasInFlightRequest() {
- return mInFlightRequest != null;
+ boolean hasCapturingRequest() {
+ return mCapturingRequest != null;
+ }
+
+ @VisibleForTesting
+ List<RequestWithCallback> getIncompleteRequests() {
+ return mIncompleteRequests;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public ImagePipeline getImagePipeline() {
+ return mImagePipeline;
}
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index f77c182..c8e398c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -16,6 +16,7 @@
package androidx.camera.core.imagecapture;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
import static androidx.core.util.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
@@ -25,9 +26,11 @@
import android.os.Build;
import androidx.annotation.IntRange;
+import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
@@ -35,6 +38,7 @@
import androidx.camera.core.impl.ImageCaptureConfig;
import androidx.camera.core.impl.ImageOutputConfig;
import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
import com.google.auto.value.AutoValue;
@@ -51,7 +55,12 @@
@AutoValue
public abstract class TakePictureRequest {
- // TODO(b/242683221): add a retry counter.
+ /**
+ * By default, ImageCapture does not retry requests. For some problematic devices, the
+ * capture request can become success after retrying. The allowed retry count will be
+ * provided by the {@link CaptureFailedRetryEnabler}.
+ */
+ private int mRemainingRetires = new CaptureFailedRetryEnabler().getRetryCount();
/**
* Gets the callback {@link Executor} provided by the app.
@@ -123,6 +132,43 @@
abstract List<CameraCaptureCallback> getSessionConfigCameraCaptureCallbacks();
/**
+ * Decrements retry counter.
+ *
+ * @return true if there is still remaining retries at the time of calling. In that case, the
+ * request should be retried. False when there is no retry left. The caller needs to fail the
+ * request.
+ */
+ @MainThread
+ boolean decrementRetryCounter() {
+ checkMainThread();
+ if (mRemainingRetires > 0) {
+ mRemainingRetires--;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Increments retry counter.
+ */
+ @MainThread
+ void incrementRetryCounter() {
+ checkMainThread();
+ mRemainingRetires++;
+ }
+
+ /**
+ * Gets the current retry count for testing.
+ */
+ @MainThread
+ @VisibleForTesting
+ int getRemainingRetries() {
+ checkMainThread();
+ return mRemainingRetires;
+ }
+
+ /**
* Delivers {@link ImageCaptureException} to the app.
*/
void onError(@NonNull ImageCaptureException imageCaptureException) {
@@ -177,4 +223,17 @@
onDiskCallback, outputFileOptions, cropRect, sensorToBufferTransform,
rotationDegrees, jpegQuality, captureMode, sessionConfigCameraCaptureCallbacks);
}
+
+ /**
+ * Interface for retrying a {@link TakePictureRequest}.
+ */
+ interface RetryControl {
+
+ /**
+ * Retries the given {@link TakePictureRequest}.
+ *
+ * <p>The request should be injected to the front of the queue.
+ */
+ void retryRequest(@NonNull TakePictureRequest takePictureRequest);
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java
new file mode 100644
index 0000000..cd4f709
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/CaptureFailedRetryQuirk.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.quirk;
+
+import android.os.Build;
+import android.util.Pair;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+import androidx.camera.core.internal.compat.workaround.CaptureFailedRetryEnabler;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: 242683221
+ * Description: Quirk that allows ImageCapture to retry once when encountering capture
+ * failures. On Samsung sm-g981u1, image capture requests might fail when continuously taking
+ * multiple pictures. The capture failure might be related to some problems in the CamX
+ * pipeline. Re-issue the original capture request to retry can workaround the issue.
+ * Device(s): Samsung sm-g981u1
+ * @see CaptureFailedRetryEnabler
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class CaptureFailedRetryQuirk implements Quirk {
+
+ private static final Set<Pair<String, String>> FAILED_RETRY_ALLOW_LIST = new HashSet<>(
+ Collections.singletonList(Pair.create("SAMSUNG", "SM-G981U1")));
+
+ static boolean load() {
+ String brand = Build.BRAND.toUpperCase(Locale.US);
+ String model = Build.MODEL.toUpperCase(Locale.US);
+
+ return FAILED_RETRY_ALLOW_LIST.contains(Pair.create(brand, model));
+ }
+
+ /**
+ * Returns the count which the image capture request can be retried. Currently returns 1
+ * always if the quirk is loaded.
+ */
+ public int getRetryCount() {
+ return 1;
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index 79ddcbb..afcaf8e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -46,6 +46,9 @@
if (SurfaceOrderQuirk.load()) {
quirks.add(new SurfaceOrderQuirk());
}
+ if (CaptureFailedRetryQuirk.load()) {
+ quirks.add(new CaptureFailedRetryQuirk());
+ }
return quirks;
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java
new file mode 100644
index 0000000..e1b88953
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnabler.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.internal.compat.quirk.CaptureFailedRetryQuirk;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+
+/**
+ * Workaround that allows the {@link ImageCapture} to retry the same capture request once when
+ * encountering capture failures.
+ *
+ * @see CaptureFailedRetryQuirk
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class CaptureFailedRetryEnabler {
+
+ private final CaptureFailedRetryQuirk mCaptureFailedRetryQuirk = DeviceQuirks.get(
+ CaptureFailedRetryQuirk.class);
+
+ /**
+ * Returns the count which the image capture request can be retried.
+ */
+ public int getRetryCount() {
+ return mCaptureFailedRetryQuirk == null ? 0 : mCaptureFailedRetryQuirk.getRetryCount();
+ }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index dad0080..6fae56c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -94,6 +94,9 @@
@Nullable
private SurfaceRequest mProviderSurfaceRequest;
+ @NonNull
+ private final Runnable mOnInvalidated;
+
/**
* Please see the getters to understand the parameters.
*/
@@ -105,7 +108,8 @@
boolean hasEmbeddedTransform,
@NonNull Rect cropRect,
int rotationDegrees,
- boolean mirroring) {
+ boolean mirroring,
+ @NonNull Runnable onInvalidated) {
super(size, format);
mTargets = targets;
mSensorToBufferTransform = sensorToBufferTransform;
@@ -113,6 +117,7 @@
mCropRect = cropRect;
mRotationDegrees = rotationDegrees;
mMirroring = mirroring;
+ mOnInvalidated = onInvalidated;
mSurfaceFuture = CallbackToFutureAdapter.getFuture(
completer -> {
mCompleter = completer;
@@ -223,8 +228,8 @@
@Nullable Range<Integer> expectedFpsRange) {
checkMainThread();
// TODO(b/238230154) figure out how to support HDR.
- SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, true,
- expectedFpsRange);
+ SurfaceRequest surfaceRequest = new SurfaceRequest(getSize(), cameraInternal, false,
+ expectedFpsRange, this::invalidate);
try {
setProvider(surfaceRequest.getDeferrableSurface());
} catch (SurfaceClosedException e) {
@@ -282,6 +287,22 @@
}
/**
+ * Invalidate the previously provided {@link Surface} to provide a new one.
+ *
+ * Calls to inform that the {@link Surface} previously provided via
+ * {@link #createSurfaceRequest(CameraInternal)} or
+ * {@link #createSurfaceRequest(CameraInternal, Range)} is no longer valid and should reacquire
+ * a new {@link Surface}.
+ *
+ * <p>The method <strong>must</strong> be called when the surface provider is ready to provide
+ * a new {@link Surface}. (e.g. SurfaceView's surface is created when its window is visible.)
+ */
+ public void invalidate() {
+ close();
+ mOnInvalidated.run();
+ }
+
+ /**
* Closes the {@link DeferrableSurface} and notifies linked objects for the closure.
*/
@Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index fd1e4bcf8..6007535 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -106,6 +106,11 @@
@NonNull
private SettableSurface createOutputSurface(@NonNull SettableSurface inputSurface) {
+ // TODO: Can be improved by only restarting part of the pipeline. e.g. only update the
+ // output Surface (between Effect/App), and still use the same input Surface (between
+ // Camera/Effect). It's just simpler for now.
+ final Runnable >
+
SettableSurface outputSurface;
switch (mGlTransformOptions) {
case APPLY_CROP_ROTATE_AND_MIRRORING:
@@ -135,7 +140,8 @@
/*hasEmbeddedTransform=*/false,
sizeToRect(rotatedCroppedSize),
/*rotationDegrees=*/0,
- /*mirroring=*/false);
+ /*mirroring=*/false,
+ onSurfaceInvalidated);
break;
case USE_SURFACE_TEXTURE_TRANSFORM:
// No transform output as placeholder.
@@ -148,7 +154,8 @@
/*hasEmbeddedTransform=*/false,
inputSurface.getCropRect(),
inputSurface.getRotationDegrees(),
- inputSurface.getMirroring());
+ inputSurface.getMirroring(),
+ onSurfaceInvalidated);
break;
default:
throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index a7f51e4..1ef77cb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -192,13 +192,32 @@
}
@Test
- fun processingPipelineOffByDefault() {
- assertThat(
- bindImageCapture(
- useProcessingPipeline = false,
- bufferFormat = ImageFormat.JPEG,
- ).isProcessingPipelineEnabled
- ).isFalse()
+ fun onError_surfaceIsRecreated() {
+ // Arrange: create ImageCapture and get the Surface
+ val imageCapture = bindImageCapture(
+ bufferFormat = ImageFormat.JPEG,
+ )
+ val oldSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+ assertTakePictureManagerHasTheSameSurface(imageCapture)
+
+ // Act: invoke onError callback.
+ imageCapture.sessionConfig.errorListeners.single().onError(
+ imageCapture.sessionConfig,
+ SessionConfig.SessionError.SESSION_ERROR_SURFACE_NEEDS_RESET
+ )
+
+ // Assert: the surface has been recreated.
+ val newSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+ assertThat(newSurface).isNotEqualTo(oldSurface)
+ assertTakePictureManagerHasTheSameSurface(imageCapture)
+ }
+
+ private fun assertTakePictureManagerHasTheSameSurface(imageCapture: ImageCapture) {
+ val takePictureManagerSurface =
+ imageCapture.takePictureManager.imagePipeline.createSessionConfigBuilder()
+ .build().surfaces.single().surface.get()
+ val useCaseSurface = imageCapture.sessionConfig.surfaces.single().surface.get()
+ assertThat(takePictureManagerSurface).isEqualTo(useCaseSurface)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
index 151ab67..c783150 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/CaptureNodeTest.kt
@@ -18,15 +18,18 @@
import android.graphics.ImageFormat
import android.os.Build
+import android.os.Looper.getMainLooper
import android.util.Size
import androidx.camera.core.ImageProxy
import androidx.camera.core.imagecapture.Utils.createCaptureBundle
import androidx.camera.core.imagecapture.Utils.createFakeImage
import com.google.common.truth.Truth.assertThat
+import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.internal.DoNotInstrument
@@ -58,10 +61,32 @@
}
}
+ @After
+ fun tearDown() {
+ captureNode.release()
+ }
+
+ @Test
+ fun release_imageReaderNotClosedUntilTermination() {
+ // Arrange: increment the DeferrableSurface's use count to prevent it from being terminated.
+ captureNode.inputEdge.surface.incrementUseCount()
+ // Act: release.
+ captureNode.release()
+ shadowOf(getMainLooper()).idle()
+ // Assert: ImageReader is not closed.
+ assertThat(captureNode.mSafeCloseImageReaderProxy!!.isClosed).isFalse()
+
+ // Act: decrease the use count. Now the DeferrableSurface will terminate.
+ captureNode.inputEdge.surface.decrementUseCount()
+ shadowOf(getMainLooper()).idle()
+ // Assert: ImageReader is closed.
+ assertThat(captureNode.mSafeCloseImageReaderProxy!!.isClosed).isTrue()
+ }
+
@Test
fun transform_verifyInputSurface() {
assertThat(captureNodeIn.surface.surface.get())
- .isEqualTo(captureNode.mSafeCloseImageReaderProxy.surface)
+ .isEqualTo(captureNode.mSafeCloseImageReaderProxy!!.surface)
}
@Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
index b1fe7d6c..26bba9a 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeImageCaptureControl.kt
@@ -18,6 +18,7 @@
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.utils.futures.Futures
+import androidx.concurrent.futures.CallbackToFutureAdapter
import com.google.common.util.concurrent.ListenableFuture
/**
@@ -25,9 +26,21 @@
*/
class FakeImageCaptureControl : ImageCaptureControl {
+ companion object {
+ // By default, this fake object returns an immediate successful result.
+ private val IMMEDIATE_RESULT: ListenableFuture<Void> = Futures.immediateFuture(null)
+ }
+
val actions = arrayListOf<Action>()
var latestCaptureConfigs: List<CaptureConfig?> = arrayListOf()
- var response: ListenableFuture<Void> = Futures.immediateFuture(null)
+
+ // Flip this flag to return a custom result using pendingResultCompleter.
+ var shouldUsePendingResult = false
+ lateinit var pendingResultCompleter: CallbackToFutureAdapter.Completer<Void>
+ var pendingResult = CallbackToFutureAdapter.getFuture { completer ->
+ pendingResultCompleter = completer
+ "FakeImageCaptureControl's pendingResult"
+ }
override fun lockFlashMode() {
actions.add(Action.LOCK_FLASH)
@@ -42,7 +55,10 @@
): ListenableFuture<Void> {
actions.add(Action.SUBMIT_REQUESTS)
latestCaptureConfigs = captureConfigs
- return response
+ if (shouldUsePendingResult) {
+ return pendingResult
+ }
+ return IMMEDIATE_RESULT
}
enum class Action {
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
similarity index 60%
copy from room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
copy to camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
index 4e95b3d..e58d031 100644
--- a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeRetryControl.kt
@@ -14,15 +14,18 @@
* limitations under the License.
*/
-package androidx.room;
+package androidx.camera.core.imagecapture
-import kotlin.coroutines.Continuation;
-import kotlin.jvm.functions.Function1;
+import androidx.camera.core.imagecapture.TakePictureRequest.RetryControl
-public class RoomDatabaseKt {
- public static final <R> Object withTransaction(RoomDatabase db,
- Function1<? super Continuation<? super R>, ? extends Object> block,
- Continuation<? super R> cont) {
- return null;
+/**
+ * Fake [RetryControl] that keeps track of the retried request.
+ */
+class FakeRetryControl : RetryControl {
+
+ var retriedRequest: TakePictureRequest? = null
+
+ override fun retryRequest(takePictureRequest: TakePictureRequest) {
+ retriedRequest = takePictureRequest
}
}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index a40665a..f2d3f38 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -291,7 +291,7 @@
for (i in 0 until MAX_IMAGES) {
val imageInfo = FakeImageInfo()
imageReaderProxy.triggerImageAvailable(imageInfo.tagBundle, 0)
- imagePipeline.captureNode.mSafeCloseImageReaderProxy.acquireNextImage()
+ imagePipeline.captureNode.mSafeCloseImageReaderProxy!!.acquireNextImage()
?.let { images.add(it) }
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
index 8789904..f0b794e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/RequestWithCallbackTest.kt
@@ -46,6 +46,7 @@
private lateinit var otherError: ImageCaptureException
private lateinit var imageResult: ImageProxy
private lateinit var fileResult: ImageCapture.OutputFileResults
+ private lateinit var retryControl: FakeRetryControl
@Before
fun setUp() {
@@ -53,15 +54,49 @@
otherError = ImageCaptureException(ERROR_CAPTURE_FAILED, "", null)
imageResult = FakeImageProxy(FakeImageInfo())
fileResult = ImageCapture.OutputFileResults(null)
+ retryControl = FakeRetryControl()
+ }
+
+ @Test
+ fun failCaptureWithRemainingRetries_requestIsRetried() {
+ // Arrange: create a request the a retry counter of 1.
+ val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ request.incrementRetryCounter()
+ assertThat(request.remainingRetries).isEqualTo(1)
+ val callback = RequestWithCallback(request, retryControl)
+
+ // Act.
+ callback.onCaptureFailure(otherError)
+ shadowOf(getMainLooper()).idle()
+
+ // Assert.
+ assertThat(retryControl.retriedRequest).isEqualTo(request)
+ assertThat(request.remainingRetries).isEqualTo(0)
+ }
+
+ @Test
+ fun failCaptureWithoutRemainingRetries_requestNotRetried() {
+ // Arrange: create a request the a retry counter of 0.
+ val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ assertThat(request.remainingRetries).isEqualTo(0)
+ val callback = RequestWithCallback(request, retryControl)
+
+ // Act.
+ callback.onCaptureFailure(otherError)
+ shadowOf(getMainLooper()).idle()
+
+ // Assert.
+ assertThat(retryControl.retriedRequest).isNull()
+ assertThat(request.exceptionReceived).isEqualTo(otherError)
}
@Test
fun abortRequestThenSendOtherErrors_receiveAbortError() {
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
- val callback = RequestWithCallback(request)
+ val callback = RequestWithCallback(request, retryControl)
// Act.
- callback.abort(abortError)
+ callback.abortAndSendErrorToApp(abortError)
callback.onCaptureFailure(otherError)
callback.onProcessFailure(otherError)
shadowOf(getMainLooper()).idle()
@@ -73,7 +108,7 @@
fun sendInMemoryResult_receiveResult() {
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
- val callback = RequestWithCallback(request)
+ val callback = RequestWithCallback(request, retryControl)
// Act.
callback.onImageCaptured()
callback.onFinalResult(imageResult)
@@ -86,9 +121,9 @@
fun abortRequestAndSendInMemoryResult_doNotReceiveResult() {
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
- val callback = RequestWithCallback(request)
+ val callback = RequestWithCallback(request, retryControl)
// Act.
- callback.abort(abortError)
+ callback.abortAndSendErrorToApp(abortError)
callback.onFinalResult(imageResult)
shadowOf(getMainLooper()).idle()
// Assert.
@@ -99,7 +134,7 @@
fun sendOnDiskResult_receiveResult() {
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
- val callback = RequestWithCallback(request)
+ val callback = RequestWithCallback(request, retryControl)
// Act.
callback.onImageCaptured()
callback.onFinalResult(fileResult)
@@ -112,9 +147,9 @@
fun abortRequestAndSendOnDiskResult_doNotReceiveResult() {
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
- val callback = RequestWithCallback(request)
+ val callback = RequestWithCallback(request, retryControl)
// Act.
- callback.abort(abortError)
+ callback.abortAndSendErrorToApp(abortError)
callback.onFinalResult(imageResult)
shadowOf(getMainLooper()).idle()
// Assert.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index 5335ee6..5ed2782 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -24,7 +24,6 @@
import androidx.camera.core.ImageCapture.OutputFileResults
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.testing.fakes.FakeImageInfo
import androidx.camera.testing.fakes.FakeImageProxy
import com.google.common.truth.Truth.assertThat
@@ -46,7 +45,9 @@
private val imagePipeline = FakeImagePipeline()
private val imageCaptureControl = FakeImageCaptureControl()
- private val takePictureManager = TakePictureManager(imageCaptureControl, imagePipeline)
+ private val takePictureManager =
+ TakePictureManager(imageCaptureControl).also { it.setImagePipeline(imagePipeline) }
+ private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
@After
fun tearDown() {
@@ -54,14 +55,55 @@
}
@Test
- fun abortRequests_receiveErrorCallback() {
+ fun pause_inFlightRequestRetried() {
+ // Arrange: create 2 requests and offer them to the manager.
+ imageCaptureControl.shouldUsePendingResult = true
+ val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ takePictureManager.offerRequest(request1)
+ takePictureManager.offerRequest(request2)
+ assertThat(takePictureManager.mNewRequests).containsExactly(request2)
+
+ // Act: pause the manager.
+ takePictureManager.pause()
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the in-flight request is retried.
+ assertThat(takePictureManager.mNewRequests).containsExactly(request1, request2).inOrder()
+ assertThat(request1.remainingRetries).isEqualTo(0)
+ }
+
+ @Test
+ fun abortPostProcessingRequests_receiveErrorCallback() {
+ // Arrange: setup a request that is captured but not processed.
+ val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+ takePictureManager.offerRequest(request)
+ val processingRequest = imagePipeline.getProcessingRequest(request)
+ processingRequest.onImageCaptured()
+ shadowOf(getMainLooper()).idle()
+ // Verify the "captured, not processed" status.
+ assertThat(takePictureManager.hasCapturingRequest()).isFalse()
+ assertThat(takePictureManager.incompleteRequests[0].captureFuture.isDone).isTrue()
+ assertThat(takePictureManager.incompleteRequests[0].completeFuture.isDone).isFalse()
+
+ // Act: abort all requests.
+ takePictureManager.abortRequests()
+ shadowOf(getMainLooper()).idle()
+
+ // Assert: the user has received a CAMERA_CLOSED error.
+ assertThat((request.exceptionReceived as ImageCaptureException).imageCaptureError)
+ .isEqualTo(ERROR_CAMERA_CLOSED)
+ }
+
+ @Test
+ fun abortNewRequests_receiveErrorCallback() {
// Arrange: send 2 request.
val request1 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
val request2 = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
takePictureManager.offerRequest(request1)
takePictureManager.offerRequest(request2)
- // Act: ab the manage and finish the 1st request.
+ // Act: abort the manager and finish the 1st request.
takePictureManager.abortRequests()
// Camera returns the image, but it should be ignored.
val processingRequest = imagePipeline.getProcessingRequest(request1)
@@ -88,7 +130,6 @@
// Arrange.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
takePictureManager.offerRequest(request)
- val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
val processingRequest = imagePipeline.getProcessingRequest(request)
processingRequest.onImageCaptured()
// Act.
@@ -147,24 +188,25 @@
fun submitRequestFailsWithImageCaptureException_appGetsTheSameException() {
// Arrange: configure ImageCaptureControl to always fail.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
- val cause = ImageCaptureException(ERROR_CAMERA_CLOSED, "", null)
- imageCaptureControl.response = Futures.immediateFailedFuture(cause)
+ imageCaptureControl.shouldUsePendingResult = true
+ imageCaptureControl.pendingResultCompleter.setException(exception)
// Act.
takePictureManager.offerRequest(request)
shadowOf(getMainLooper()).idle()
// Assert.
- assertThat(request.exceptionReceived!!).isEqualTo(cause)
- assertThat(takePictureManager.hasInFlightRequest()).isFalse()
+ assertThat(request.exceptionReceived!!).isEqualTo(exception)
+ assertThat(takePictureManager.hasCapturingRequest()).isFalse()
}
@Test
fun submitRequestFailsWithGenericException_appGetsWrappedException() {
// Arrange: configure ImageCaptureControl to always fail.
val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
- val cause = Exception()
- imageCaptureControl.response = Futures.immediateFailedFuture(cause)
+ val genericException = Exception()
+ imageCaptureControl.shouldUsePendingResult = true
+ imageCaptureControl.pendingResultCompleter.setException(genericException)
// Act.
takePictureManager.offerRequest(request)
@@ -172,8 +214,8 @@
// Assert.
assertThat(request.exceptionReceived!!.imageCaptureError).isEqualTo(ERROR_CAPTURE_FAILED)
- assertThat(request.exceptionReceived!!.cause).isEqualTo(cause)
- assertThat(takePictureManager.hasInFlightRequest()).isFalse()
+ assertThat(request.exceptionReceived!!.cause).isEqualTo(genericException)
+ assertThat(takePictureManager.hasCapturingRequest()).isFalse()
}
@Test
@@ -233,7 +275,7 @@
takePictureManager.offerRequest(request2)
takePictureManager.offerRequest(request3)
shadowOf(getMainLooper()).idle()
- val response1 = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
+ val response1 = exception
val response2 = OutputFileResults(null)
val response3 = FakeImageProxy(FakeImageInfo())
imagePipeline.getProcessingRequest(request1).onImageCaptured()
@@ -259,7 +301,6 @@
takePictureManager.offerRequest(request)
// Act: send exception via ImagePipeline
- val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
imagePipeline.getProcessingRequest(request).onImageCaptured()
imagePipeline.getProcessingRequest(request).onProcessFailure(exception)
shadowOf(getMainLooper()).idle()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt
new file mode 100644
index 0000000..74b8ffe
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/compat/workaround/CaptureFailedRetryEnablerTest.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.internal.compat.workaround
+
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers
+
+/**
+ * Unit test for [CaptureFailedRetryEnabler]
+ */
+@SmallTest
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class CaptureFailedRetryEnablerTest(
+ private val brand: String,
+ private val model: String,
+ private val expectedResult: Int
+) {
+
+ companion object {
+ @JvmStatic
+ @Parameters(name = "brand={0}, model={1}, expectedResult={2}")
+ fun data() = mutableListOf<Array<Any?>>().apply {
+ add(arrayOf("SAMSUNG", "SM-G981U1", 1))
+ add(arrayOf("samsung", "sm-g981u1", 1))
+ add(arrayOf("samsung", "sm-g981u10", 0))
+ add(arrayOf("samsung", "sm-g981u", 0))
+ add(arrayOf("fakeBrand", "sm-g981u1", 0))
+ add(arrayOf("", "", 0))
+ }
+ }
+
+ @Before
+ fun setup() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", brand)
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", model)
+ }
+
+ @Test
+ fun shouldRetry() {
+ assertThat(CaptureFailedRetryEnabler().retryCount).isEqualTo(expectedResult)
+ }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
index 64bebab..96048cd 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
@@ -72,7 +72,7 @@
settableSurface = SettableSurface(
CameraEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
Matrix(), true, Rect(), 0, false
- )
+ ) {}
fakeSurfaceTexture = SurfaceTexture(0)
fakeSurface = Surface(fakeSurfaceTexture)
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 0724a8d..3027eceb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -265,7 +265,7 @@
cropRect,
rotationDegrees,
mirroring
- )
+ ) {}
inputEdge = SurfaceEdge.create(listOf(surface))
}
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
index b07183a..9047555 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorWithExecutorTest.kt
@@ -93,7 +93,7 @@
}
}, executor)
// Act: invoke methods.
- processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera(), false))
+ processorWithExecutor.onInputSurface(SurfaceRequest(SIZE, FakeCamera(), false) {})
processorWithExecutor.onOutputSurface(mock(SurfaceOutput::class.java))
shadowOf(getMainLooper()).idle()
shadowOf(executorThread.looper).idle()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
new file mode 100644
index 0000000..7d8d4ae
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockConsumer.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast;
+import androidx.core.util.Consumer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock implementation of {@link Consumer} to verify proper invocations in tests.
+ *
+ * @param <T> the parameter type to be consumed
+ */
+public class MockConsumer<T> implements Consumer<T> {
+
+ private CountDownLatch mLatch;
+
+ private final List<T> mEventList = new ArrayList<>();
+ private int mIndexLastVerifiedInOrder = -1;
+
+ private Class<?> mClassTypeToVerify;
+ private CallTimes mCallTimes;
+ private boolean mInOrder = false;
+
+ private int getEventCount() {
+ int count = 0;
+ int startIndex = mInOrder ? mIndexLastVerifiedInOrder + 1 : 0;
+ for (int i = startIndex; i < mEventList.size(); i++) {
+ if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ private int getLastVerifiedEventInOrder() {
+ int count = 0;
+ for (int i = mIndexLastVerifiedInOrder + 1; i < mEventList.size(); i++) {
+ if (mClassTypeToVerify.isInstance(mEventList.get(i))) {
+ count++;
+ }
+
+ if (mCallTimes.isSatisfied(count)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private boolean isVerified() {
+ if (mClassTypeToVerify == null || mCallTimes == null) {
+ return false;
+ }
+
+ return mCallTimes.isSatisfied(getEventCount());
+ }
+
+ /**
+ * Verifies if {@link #accept} method was invoked properly during test.
+ *
+ * @param captor the argument captor to record the arguments passed to {@link #accept} method
+ * matching the provided {@code classType}
+ * @see #verifyAcceptCall(Class, boolean, long, CallTimes)
+ */
+ public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+ @NonNull CallTimes callTimes, @Nullable ArgumentCaptor<T> captor) {
+ mClassTypeToVerify = classType;
+ mCallTimes = callTimes;
+ mInOrder = inOrder;
+
+ assertTrue("accept() called " + getEventCount() + " time(s) with "
+ + classType.getSimpleName() + (inOrder ? " in order" : "") + ", expected "
+ + (callTimes instanceof CallTimesAtLeast ? " at least " : "")
+ + callTimes.getTimes() + " times",
+ isVerified());
+
+ if (inOrder) {
+ mIndexLastVerifiedInOrder = getLastVerifiedEventInOrder();
+ }
+
+ if (captor != null) {
+ captor.setArguments(mEventList);
+ }
+ }
+
+ /**
+ * Verifies if {@link #accept} method was invoked properly during test.
+ *
+ * <p>{@code callTimes} defaults to {@code new CallTimes(1)} which ensures that
+ * {@link #accept} method was invoked exactly once with given verification parameters.
+ *
+ * @see #verifyAcceptCall(Class, boolean, long, CallTimes)
+ */
+ public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+ long timeoutInMillis) {
+ verifyAcceptCall(classType, inOrder, timeoutInMillis, new CallTimes(1));
+ }
+
+ /**
+ * Verifies if {@link #accept} method was invoked properly during test.
+ *
+ * <p>Usually invoked from a method with {@link org.junit.Test} annotation.
+ *
+ * @param classType the class type to verify for the parameter of {@link #accept(Object)}
+ * method, this is checked with {@link Class#isInstance(Object)} method
+ * @param inOrder the {@link #verifyAcceptCall} method invocations with {@code inOrder =
+ * true} are chained together to make sure they were in order
+ * @param timeoutInMillis the time limit to wait for asynchronous operation after which
+ * {@link junit.framework.AssertionFailedError} is thrown
+ * @param callTimes the condition for how many times {@link #accept} method should be
+ * called
+ */
+ public void verifyAcceptCall(@NonNull Class<?> classType, boolean inOrder,
+ long timeoutInMillis, @NonNull CallTimes callTimes) {
+ mClassTypeToVerify = classType;
+ mCallTimes = callTimes;
+ mInOrder = inOrder;
+
+ if (!isVerified()) {
+ mLatch = new CountDownLatch(1);
+
+ try {
+ assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
+ mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ assertTrue("accept() called " + getEventCount() + " time(s) with "
+ + classType.getSimpleName()
+ + (mInOrder ? " in order" : "") + ", expected "
+ + (callTimes instanceof CallTimesAtLeast ? " at least " : "")
+ + callTimes.getTimes() + " times",
+ isVerified());
+
+ if (inOrder) {
+ mIndexLastVerifiedInOrder = getLastVerifiedEventInOrder();
+ }
+
+ mLatch = null;
+ }
+ }
+
+ @Override
+ public void accept(T event) {
+ mEventList.add(event);
+
+ if (mLatch != null && isVerified()) {
+ mLatch.countDown();
+ }
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java
new file mode 100644
index 0000000..bb54016
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/MockObserver.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static junit.framework.TestCase.assertTrue;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.impl.Observable;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock implementation of {@link Observable.Observer} to verify proper invocations in tests.
+ *
+ * @param <T> the parameter type to be observed
+ */
+public class MockObserver<T> implements Observable.Observer<T> {
+ private final Map<T, Integer> mNewDataCount = new HashMap<>();
+
+ private CallTimes mCallTimes;
+ private T mValueToVerify;
+ private CountDownLatch mLatch;
+
+ private boolean isVerified() {
+ if (mCallTimes == null) {
+ return false;
+ }
+
+ int count = mNewDataCount.get(mValueToVerify) == null
+ ? 0 : mNewDataCount.get(mValueToVerify);
+ return mCallTimes.isSatisfied(count);
+ }
+
+ /**
+ * Verifies if {@link #onNewData} method was invoked properly during test.
+ *
+ * <p>{@code callTimes} defaults to {@code new CallTimes(1)} which ensures that
+ * {@link #onNewData(Object)} method was invoked exactly once with given verification
+ * parameters.
+ *
+ * @see #verifyOnNewDataCall(Object, long, CallTimes)
+ */
+ public void verifyOnNewDataCall(@Nullable T value,
+ long timeoutInMillis) {
+ verifyOnNewDataCall(value, timeoutInMillis, new CallTimes(1));
+ }
+
+ /**
+ * Verifies if {@link #onNewData} method was invoked properly during test.
+ *
+ * @param value the value to match with the value parameter of {@link #onNewData(Object)}
+ * @param timeoutInMillis the time limit to wait for asynchronous operation after which
+ * {@link junit.framework.AssertionFailedError} is thrown
+ * @param callTimes the condition for how many times {@link #onNewData} method should be called
+ */
+ public void verifyOnNewDataCall(@Nullable T value,
+ long timeoutInMillis, @NonNull CallTimes callTimes) {
+ mValueToVerify = value;
+ mCallTimes = callTimes;
+
+ if (!isVerified()) {
+ mLatch = new CountDownLatch(1);
+
+ try {
+ assertTrue("Test failed for a timeout of " + timeoutInMillis + " ms",
+ mLatch.await(timeoutInMillis, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ assertTrue("onNewData() called "
+ + mNewDataCount.get(value) + " time(s) with " + value,
+ isVerified());
+
+ mLatch = null;
+ }
+ }
+
+ @Override
+ public void onNewData(@Nullable T value) {
+ Integer prevCount = mNewDataCount.get(value);
+ mNewDataCount.put(value, (prevCount == null ? 0 : prevCount) + 1);
+
+ if (mLatch != null && isVerified()) {
+ mLatch.countDown();
+ }
+ }
+
+ @Override
+ public void onError(@NonNull Throwable t) {}
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java
new file mode 100644
index 0000000..59b71ba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentCaptor.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility for capturing arguments, usually in fake class method invocations for testing.
+ *
+ * @param <T> the type of the arguments to capture
+ */
+public class ArgumentCaptor<T> {
+ private final List<T> mArguments = new ArrayList<>();
+ private ArgumentMatcher<T> mArgumentMatcher;
+
+ /**
+ * Creates a new instance of {@link ArgumentCaptor}.
+ */
+ public ArgumentCaptor() {}
+
+ /**
+ * Creates a new instance of {@link ArgumentCaptor} with the given parameter.
+ *
+ * @param argumentMatcher specifies the matching criteria for capturing
+ */
+ public ArgumentCaptor(@NonNull ArgumentMatcher<T> argumentMatcher) {
+ mArgumentMatcher = argumentMatcher;
+ }
+
+ /**
+ * Returns the last value captured, {@code null} if no value has been captured yet
+ */
+ @Nullable
+ public T getValue() {
+ if (mArguments.size() == 0) {
+ return null;
+ }
+
+ return mArguments.get(mArguments.size() - 1);
+ }
+
+ /**
+ * Adds arguments to capture list according to argument matching rule (if exists).
+ *
+ * @param argumentList the list of arguments to capture
+ */
+ public void setArguments(@NonNull List<T> argumentList) {
+ for (T argument : argumentList) {
+ if (mArgumentMatcher == null || mArgumentMatcher.matches(argument)) {
+ mArguments.add(argument);
+ }
+ }
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java
new file mode 100644
index 0000000..c95163a
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/ArgumentMatcher.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * An interface for matching arguments in {@link ArgumentCaptor} class.
+ *
+ * @param <T> the type of the arguments to capture
+ */
+public interface ArgumentMatcher<T> {
+
+ /**
+ * Matches an argument according to matching criteria and returns if it is matched or not.
+ *
+ * @param argument the argument to match according to criteria
+ * @return {@code true} if argument is matched according desired criteria,
+ * {@code false} otherwise
+ */
+ boolean matches(T argument);
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java
new file mode 100644
index 0000000..8601807
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimes.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * Utility for defining the number of invocations allowed while testing fake class methods.
+ */
+public class CallTimes {
+ protected int mTimes;
+
+ /**
+ * Creates a new instance of {@link CallTimes} with the given parameter.
+ *
+ * @param times the number of invocations allowed
+ */
+ public CallTimes(int times) {
+ mTimes = times;
+ }
+
+ public int getTimes() {
+ return mTimes;
+ }
+
+ /**
+ * Checks if the number of invocation is exactly the same as specified.
+ *
+ * @param actualCallCount the occurred number of invocations
+ *
+ * @return {@code true} if the number of invocations is exactly the same as specified,
+ * {@code false} otherwise
+ */
+ public boolean isSatisfied(int actualCallCount) {
+ return actualCallCount == mTimes;
+ }
+}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java
new file mode 100644
index 0000000..5b78740
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeast.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+/**
+ * Utility for defining the number of invocations allowed while testing fake class methods.
+ */
+public class CallTimesAtLeast extends CallTimes {
+ /**
+ * Creates a new instance of {@link CallTimesAtLeast} with the given parameter.
+ *
+ * @param times the minimum number of invocations that should be occurring
+ */
+ public CallTimesAtLeast(int times) {
+ super(times);
+ }
+
+ /**
+ * Checks if the number of invocation is at least {@link #mTimes}.
+ *
+ * @param actualCallCount the occurred number of invocations
+ * @return {@code true} if the number of invocations is at least the times specified,
+ * {@code false} otherwise
+ */
+ @Override
+ public boolean isSatisfied(int actualCallCount) {
+ return actualCallCount >= mTimes;
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java
new file mode 100644
index 0000000..fc01c74
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockConsumerTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor;
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockConsumerTest {
+ public static final String DUMMY_STRING_1 = "dummy1";
+ public static final String DUMMY_STRING_2 = "dummy2";
+
+ public static final int DUMMY_INT_1 = 1;
+
+ private MockConsumer<Object> mMockConsumer;
+
+ @Before
+ public void setUp() {
+ mMockConsumer = new MockConsumer<>();
+ }
+
+ @Test
+ public void callTimesMatches_verifyAcceptCallPasses() {
+ mMockConsumer.accept(new Object());
+ mMockConsumer.accept(new Object());
+ mMockConsumer.verifyAcceptCall(Object.class, false, new CallTimes(2), null);
+ }
+
+ @Test
+ public void callTimesMismatched_verifyAcceptCallFailsWithProperMessage() {
+ mMockConsumer.accept(new Object());
+ mMockConsumer.accept(new Object());
+
+ AssertionFailedError error = assertThrows(AssertionFailedError.class,
+ () -> mMockConsumer.verifyAcceptCall(Object.class, false, new CallTimes(1), null));
+ assertEquals("accept() called 2 time(s) with Object, expected 1 times", error.getMessage());
+ }
+
+ @Test
+ public void verifiedInOrder_verifyAcceptCallSucceeds() {
+ mMockConsumer.accept(DUMMY_STRING_1);
+ mMockConsumer.accept(DUMMY_STRING_1);
+ mMockConsumer.accept(DUMMY_INT_1);
+ mMockConsumer.accept(DUMMY_INT_1);
+ mMockConsumer.accept(DUMMY_INT_1);
+
+ mMockConsumer.verifyAcceptCall(String.class, true, new CallTimes(2), null);
+ mMockConsumer.verifyAcceptCall(Integer.class, true, new CallTimes(3), null);
+ }
+
+ @Test
+ public void verifiedInOrderWithoutConsecutiveness_verifyAcceptCallSucceeds() {
+ mMockConsumer.accept(DUMMY_STRING_1);
+ mMockConsumer.accept(DUMMY_INT_1);
+ mMockConsumer.accept(DUMMY_STRING_1);
+
+ mMockConsumer.verifyAcceptCall(String.class, true, new CallTimes(2), null);
+ }
+
+ @Test
+ public void verifiedWithArgumentCaptor_captorHasCorrectValue() {
+ ArgumentCaptor<Object> captor = new ArgumentCaptor<>();
+
+ mMockConsumer.accept(DUMMY_STRING_1);
+ mMockConsumer.accept(DUMMY_STRING_2);
+
+ mMockConsumer.verifyAcceptCall(String.class, false, new CallTimes(2), captor);
+
+ assertEquals(DUMMY_STRING_2, captor.getValue());
+ }
+
+ @Test
+ public void acceptCalledWithinTimeout_verifyAcceptCallPasses() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ mMockConsumer.accept(DUMMY_STRING_1);
+ }, "test thread").start();
+
+ mMockConsumer.verifyAcceptCall(String.class, false, 500);
+ }
+
+ @Test
+ public void acceptCalledAfterTimeout_verifyAcceptCallFailsWithProperMessage() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ mMockConsumer.accept(DUMMY_STRING_1);
+ }, "test thread").start();
+
+ AssertionFailedError error = assertThrows(AssertionFailedError.class,
+ () -> {
+ mMockConsumer.verifyAcceptCall(String.class, false, 500);
+ });
+
+ assertEquals("Test failed for a timeout of 500 ms", error.getMessage());
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java
new file mode 100644
index 0000000..4653944
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/MockObserverTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import androidx.camera.testing.mocks.helpers.CallTimes;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class MockObserverTest {
+ private static final Object DUMMY_NEW_DATA = new Object();
+ private static final Object DUMMY_NEW_DATA_2 = new Object();
+
+ private MockObserver<Object> mMockObserver;
+
+ @Before
+ public void setUp() {
+ mMockObserver = new MockObserver<>();
+ }
+
+ @Test
+ public void callTimesMatches_verifyOnNewDataCallPasses() {
+ mMockObserver.onNewData(DUMMY_NEW_DATA);
+ mMockObserver.onNewData(DUMMY_NEW_DATA);
+ mMockObserver.onNewData(DUMMY_NEW_DATA_2);
+
+ mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 100, new CallTimes(2));
+ }
+
+ @Test
+ public void verifiedWithinTimeout_verifyAcceptCallPasses() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ mMockObserver.onNewData(DUMMY_NEW_DATA);
+ }, "test thread").start();
+
+ mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 500);
+ }
+
+
+ @Test
+ public void notVerifiedWithinTimeout_verifyOnNewDataCallFailsWithProperMessage() {
+ mMockObserver.onNewData(DUMMY_NEW_DATA);
+
+ AssertionFailedError error = assertThrows(AssertionFailedError.class,
+ () -> mMockObserver.verifyOnNewDataCall(DUMMY_NEW_DATA, 100, new CallTimes(2)));
+ assertEquals("Test failed for a timeout of 100 ms",
+ error.getMessage());
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java
new file mode 100644
index 0000000..5ef9c2b
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/ArgumentCaptorTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ArgumentCaptorTest {
+ private static final Object DUMMY_ARGUMENT_1 = new Object();
+ private static final Object DUMMY_ARGUMENT_2 = new Object();
+
+ private static final List<Object> DUMMY_ARGUMENTS_1 = Arrays.asList(DUMMY_ARGUMENT_1,
+ new Object(), DUMMY_ARGUMENT_2, new Object());
+ private static final List<Object> DUMMY_ARGUMENTS_2 = Arrays.asList(new Object(), new Object());
+
+ private ArgumentCaptor<Object> mArgumentCaptor;
+
+ @Before
+ public void setUp() {
+ mArgumentCaptor = new ArgumentCaptor<>();
+ }
+
+ @Test
+ public void noArgumentProvided_getValueReturnsNull() {
+ assertNull(mArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void argumentsProvided_getValueReturnsLastValue() {
+ mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+ assertEquals(DUMMY_ARGUMENTS_1.get(DUMMY_ARGUMENTS_1.size() - 1),
+ mArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void argumentsProvidedTwice_getValueReturnsLastValue() {
+ mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+ mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_2);
+
+ assertEquals(DUMMY_ARGUMENTS_2.get(DUMMY_ARGUMENTS_2.size() - 1),
+ mArgumentCaptor.getValue());
+ }
+
+
+ @Test
+ public void argumentsProvidedWithMatcher_getValueReturnsLastMatchedValue() {
+ mArgumentCaptor = new ArgumentCaptor<>(argument -> argument.equals(DUMMY_ARGUMENT_1)
+ || argument.equals(DUMMY_ARGUMENT_2));
+
+ mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+ assertEquals(DUMMY_ARGUMENT_2, mArgumentCaptor.getValue());
+ }
+
+ @Test
+ public void argumentsProvidedWithMatcher_getValueReturnsNullForNoMatch() {
+ mArgumentCaptor = new ArgumentCaptor<>(argument -> false);
+
+ mArgumentCaptor.setArguments(DUMMY_ARGUMENTS_1);
+
+ assertNull(mArgumentCaptor.getValue());
+ }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java
new file mode 100644
index 0000000..d4bbd0c
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesAtLeastTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class CallTimesAtLeastTest {
+ private final CallTimesAtLeast mCallTimes = new CallTimesAtLeast(5);
+
+ @Test
+ public void actualCallCountMatchesExactly_isSatisfiedReturnsTrue() {
+ assertTrue(mCallTimes.isSatisfied(5));
+ }
+
+ @Test
+ public void actualCallCountIsLess_isSatisfiedReturnsFalse() {
+ assertFalse(mCallTimes.isSatisfied(2));
+ }
+
+ @Test
+ public void actualCallCountIsMore_isSatisfiedReturnsTrue() {
+ assertTrue(mCallTimes.isSatisfied(8));
+ }
+
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java
new file mode 100644
index 0000000..659bec8
--- /dev/null
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/mocks/helpers/CallTimesTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.mocks.helpers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class CallTimesTest {
+ private final CallTimes mCallTimes = new CallTimes(5);
+
+ @Test
+ public void getTimesReturnsCorrectly() {
+ assertEquals(5, mCallTimes.getTimes());
+ }
+
+ @Test
+ public void actualCallCountMatchesExactly_isSatisfiedReturnsTrue() {
+ assertTrue(mCallTimes.isSatisfied(5));
+ }
+
+ @Test
+ public void actualCallCountIsLess_isSatisfiedReturnsFalse() {
+ assertFalse(mCallTimes.isSatisfied(2));
+ }
+
+ @Test
+ public void actualCallCountIsMore_isSatisfiedReturnsFalse() {
+ assertFalse(mCallTimes.isSatisfied(8));
+ }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 2f0af13..2f648b8 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -16,6 +16,7 @@
package androidx.camera.video
+import androidx.camera.testing.mocks.helpers.ArgumentCaptor as ArgumentCaptorCameraX
import android.Manifest
import android.annotation.SuppressLint
import android.app.AppOpsManager
@@ -52,6 +53,8 @@
import androidx.camera.testing.LabTestRule
import androidx.camera.testing.SurfaceTextureProvider
import androidx.camera.testing.asFlow
+import androidx.camera.testing.mocks.MockConsumer
+import androidx.camera.testing.mocks.helpers.CallTimesAtLeast
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INVALID_OUTPUT_OPTIONS
@@ -87,7 +90,6 @@
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
@@ -259,7 +261,8 @@
@Test
fun canRecordToFile() {
- clearInvocations(videoRecordEventListener)
+ val videoRecordEventListener = MockConsumer<VideoRecordEvent>()
+
invokeSurfaceRequest()
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
@@ -268,23 +271,30 @@
.withAudioEnabled()
.start(CameraXExecutors.directExecutor(), videoRecordEventListener)
- val inOrder = inOrder(videoRecordEventListener)
- inOrder.verify(videoRecordEventListener, timeout(5000L))
- .accept(any(VideoRecordEvent.Start::class.java))
- inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
- .accept(any(VideoRecordEvent.Status::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Start::class.java,
+ true, 5000L)
+
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+ true, 15000L, CallTimesAtLeast(5))
recording.stopSafely()
- inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
- .accept(any(VideoRecordEvent.Finalize::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+ true, FINALIZE_TIMEOUT)
val uri = Uri.fromFile(file)
checkFileHasAudioAndVideo(uri)
// Check the output Uri from the finalize event match the Uri from the given file.
- val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
- verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+ val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+ VideoRecordEvent::class.java.isInstance(
+ argument
+ )
+ }
+
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java, false,
+ CallTimesAtLeast(1), captor)
+
val finalize = captor.value as VideoRecordEvent.Finalize
assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
@@ -415,9 +425,10 @@
}
@Test
- @Ignore("b/239752223")
fun canPauseResume() {
- clearInvocations(videoRecordEventListener)
+ val videoRecordEventListener =
+ MockConsumer<VideoRecordEvent>()
+
invokeSurfaceRequest()
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
@@ -429,23 +440,23 @@
recording.pause()
- val inOrder = inOrder(videoRecordEventListener)
- inOrder.verify(videoRecordEventListener, timeout(5000L))
- .accept(any(VideoRecordEvent.Pause::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Pause::class.java,
+ true, 5000L)
recording.resume()
- inOrder.verify(videoRecordEventListener, timeout(5000L))
- .accept(any(VideoRecordEvent.Resume::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Resume::class.java,
+ true, 5000L)
+
// Check there are data being encoded after resuming.
- inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
- .accept(any(VideoRecordEvent.Status::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+ true, 15000L, CallTimesAtLeast(5))
recording.stopSafely()
// Wait for the recording to be finalized.
- inOrder.verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
- .accept(any(VideoRecordEvent.Finalize::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+ true, FINALIZE_TIMEOUT)
checkFileHasAudioAndVideo(Uri.fromFile(file))
@@ -555,7 +566,6 @@
}
@Test
- @Ignore("b/239752223")
fun setFileSizeLimit() {
val fileSizeLimit = 500L * 1024L // 500 KB
runFileSizeLimitTest(fileSizeLimit)
@@ -565,7 +575,6 @@
// the encoder. This will ensure that the recording will be finalized even if it has no data
// written to it.
@Test
- @Ignore("b/239752223")
fun setFileSizeLimitLowerThanInitialDataSize() {
val fileSizeLimit = 1L // 1 byte
runFileSizeLimitTest(fileSizeLimit)
@@ -1039,9 +1048,10 @@
}
@Test
- @Ignore("b/239752223")
fun canRecordWithoutAudio() {
- clearInvocations(videoRecordEventListener)
+ val videoRecordEventListener =
+ MockConsumer<VideoRecordEvent>()
+
invokeSurfaceRequest()
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
@@ -1049,15 +1059,22 @@
recorder.prepareRecording(context, FileOutputOptions.Builder(file).build())
.start(CameraXExecutors.directExecutor(), videoRecordEventListener)
- val inOrder = inOrder(videoRecordEventListener)
- inOrder.verify(videoRecordEventListener, timeout(5000L))
- .accept(any(VideoRecordEvent.Start::class.java))
- inOrder.verify(videoRecordEventListener, timeout(15000L).atLeast(5))
- .accept(any(VideoRecordEvent.Status::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Start::class.java,
+ true, 5000L)
+
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Status::class.java,
+ true, 15000L, CallTimesAtLeast(5))
// Check the audio information reports state as disabled.
- val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
- verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+ val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+ VideoRecordEvent::class.java.isInstance(
+ argument
+ )
+ }
+
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
+ false, CallTimesAtLeast(1), captor)
+
assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
val status = captor.value as VideoRecordEvent.Status
assertThat(status.recordingStats.audioStats.audioState)
@@ -1065,8 +1082,8 @@
recording.stopSafely()
- verify(videoRecordEventListener, timeout(FINALIZE_TIMEOUT))
- .accept(any(VideoRecordEvent.Finalize::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+ false, FINALIZE_TIMEOUT)
checkFileAudio(Uri.fromFile(file), false)
checkFileVideo(Uri.fromFile(file), true)
@@ -1324,18 +1341,23 @@
.setFileSizeLimit(fileSizeLimit)
.build()
+ val videoRecordEventListener =
+ MockConsumer<VideoRecordEvent>()
+
val recording = recorder
.prepareRecording(context, outputOptions)
.withAudioEnabled()
.start(CameraXExecutors.directExecutor(), videoRecordEventListener)
- verify(
- videoRecordEventListener,
- timeout(60000L)
- ).accept(any(VideoRecordEvent.Finalize::class.java))
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent.Finalize::class.java,
+ false, 60000L)
- val captor = ArgumentCaptor.forClass(VideoRecordEvent::class.java)
- verify(videoRecordEventListener, atLeastOnce()).accept(captor.capture())
+ val captor = ArgumentCaptorCameraX<VideoRecordEvent> {
+ argument -> VideoRecordEvent::class.java.isInstance(argument)
+ }
+
+ videoRecordEventListener.verifyAcceptCall(VideoRecordEvent::class.java,
+ false, CallTimesAtLeast(1), captor)
assertThat(captor.value).isInstanceOf(VideoRecordEvent.Finalize::class.java)
val finalize = captor.value as VideoRecordEvent.Finalize
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index d0eea0a..e3f640c 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -41,6 +41,8 @@
import androidx.camera.core.CameraSelector
import androidx.camera.core.CameraXConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.processing.DefaultSurfaceProcessor
+import androidx.camera.core.processing.SurfaceProcessorInternal
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
@@ -50,7 +52,7 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -130,6 +132,7 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context: Context = ApplicationProvider.getApplicationContext()
+ private val surfaceProcessorsToRelease = mutableListOf<SurfaceProcessorInternal>()
private lateinit var cameraProvider: ProcessCameraProvider
private lateinit var lifecycleOwner: FakeLifecycleOwner
private lateinit var cameraInfo: CameraInfo
@@ -172,20 +175,42 @@
}
cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
+ for (surfaceProcessor in surfaceProcessorsToRelease) {
+ surfaceProcessor.release()
+ }
+ surfaceProcessorsToRelease.clear()
}
@Test
fun qualityOptionCanRecordVideo() {
+ testQualityOptionRecordVideo()
+ }
+
+ @Test
+ fun qualityOptionCanRecordVideo_enableSurfaceProcessor() {
+ assumeSuccessfulSurfaceProcessing()
+
+ testQualityOptionRecordVideo(surfaceProcessor = createSurfaceProcessor())
+ }
+
+ private fun testQualityOptionRecordVideo(surfaceProcessor: SurfaceProcessorInternal? = null) {
// Arrange.
val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(quality)).build()
val videoCapture = VideoCapture.withOutput(recorder)
+ videoCapture.setProcessor(surfaceProcessor)
val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
- val latchForVideoRecording = CountDownLatch(5)
- val eventListener = Consumer<VideoRecordEvent> {
- when (it) {
+ val latchForRecordingStatus = CountDownLatch(5)
+ val latchForRecordingFinalized = CountDownLatch(1)
+ var finalizedEvent: VideoRecordEvent.Finalize? = null
+ val eventListener = Consumer<VideoRecordEvent> { event ->
+ when (event) {
is VideoRecordEvent.Status -> {
// Make sure the recording proceed for a while.
- latchForVideoRecording.countDown()
+ latchForRecordingStatus.countDown()
+ }
+ is VideoRecordEvent.Finalize -> {
+ finalizedEvent = event
+ latchForRecordingFinalized.countDown()
}
else -> {
// Ignore other events.
@@ -203,20 +228,30 @@
// Act.
videoCapture.startVideoRecording(file, eventListener).use {
-
// Verify the recording proceed for a while.
- Truth.assertThat(
- latchForVideoRecording.await(
- VIDEO_TIMEOUT_SEC,
- TimeUnit.SECONDS
- )
- ).isTrue()
+ assertThat(latchForRecordingStatus.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
}
+ // Verify the recording is finalized without error.
+ assertThat(latchForRecordingFinalized.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+ assertThat(finalizedEvent!!.error).isEqualTo(VideoRecordEvent.Finalize.ERROR_NONE)
+
// Clean up
file.delete()
}
+ private fun createSurfaceProcessor(): SurfaceProcessorInternal =
+ DefaultSurfaceProcessor().apply { surfaceProcessorsToRelease.add(this) }
+
+ /** Skips tests which will enable surface processing and encounter device specific issues. */
+ private fun assumeSuccessfulSurfaceProcessing() {
+ // Skip for b/253211491
+ Assume.assumeFalse(
+ "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+ Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+ )
+ }
+
private fun VideoCapture<Recorder>.startVideoRecording(
file: File,
eventListener: Consumer<VideoRecordEvent>
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 4f61481..2df259f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -497,6 +497,11 @@
Threads.checkMainThread();
CameraInternal camera = Preconditions.checkNotNull(getCamera());
+ // Currently, VideoCapture uses StreamInfo to handle requests for surface, so
+ // handleInvalidate() is not used. But if a different approach is asked in the future,
+ // handleInvalidate() can be used as an alternative.
+ Runnable >
+
// TODO(b/229410005): The expected FPS range will need to come from the camera rather
// than what is requested in the config. For now we use the default range of (30, 30)
// for behavioral consistency.
@@ -520,7 +525,8 @@
/*hasEmbeddedTransform=*/true,
mCropRect,
getRelativeRotation(camera),
- /*mirroring=*/false);
+ /*mirroring=*/false,
+ onSurfaceInvalidated);
SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
SurfaceEdge outputEdge = mNode.transform(inputEdge);
SettableSurface appSurface = outputEdge.getSurfaces().get(0);
@@ -534,7 +540,8 @@
}
}, CameraXExecutors.mainThreadExecutor());
} else {
- mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange);
+ mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange,
+ onSurfaceInvalidated);
mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
// When camera buffers from a REALTIME device are passed directly to a video encoder
// from the camera, automatic compensation is done to account for differing timebases
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 2a9f7f3..41b0c0e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -89,9 +89,6 @@
if (ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.load()) {
quirks.add(new ImageCaptureFailedWhenVideoCaptureIsBoundQuirk());
}
- if (MediaCodecDoesNotSendEos.load()) {
- quirks.add(new MediaCodecDoesNotSendEos());
- }
return quirks;
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
deleted file mode 100644
index 46ccb85..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.video.internal.compat.quirk;
-
-import android.media.MediaCodec;
-import android.os.Build;
-
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.impl.Quirk;
-
-/**
- * <p>QuirkSummary
- * Bug Id: b/248189542
- * Description: When recording video with effect pipeline enabled, calling
- * {@link MediaCodec#signalEndOfInputStream()} doesn't trigger an EOS buffer to
- * {@link MediaCodec.Callback}.
- * Device(s): twist 2 pro.
- */
-@RequiresApi(21)
-public class MediaCodecDoesNotSendEos implements Quirk {
-
- public static boolean isPositivoTwist2Pro() {
- return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
- Build.MODEL);
- }
-
- static boolean load() {
- return isPositivoTwist2Pro();
- }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index c5d5bf1..b618ac7 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -30,6 +30,7 @@
import android.annotation.SuppressLint;
import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Bundle;
@@ -52,7 +53,6 @@
import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
-import androidx.camera.video.internal.compat.quirk.MediaCodecDoesNotSendEos;
import androidx.camera.video.internal.compat.quirk.VideoEncoderSuspendDoesNotIncludeSuspendTimeQuirk;
import androidx.camera.video.internal.workaround.EncoderFinder;
import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
@@ -202,11 +202,10 @@
Long mLastDataStopTimestamp = null;
@SuppressWarnings("WeakerAccess") // synthetic accessor
Future<?> mStopTimeoutFuture = null;
- @Nullable
- private MediaCodecCallback mMediaCodecCallback;
private boolean mIsFlushedAfterEndOfStream = false;
private boolean mSourceStoppedSignalled = false;
+ boolean mMediaCodecEosSignalled = false;
final EncoderFinder mEncoderFinder = new EncoderFinder();
@@ -277,13 +276,13 @@
mMediaCodec.reset();
mIsFlushedAfterEndOfStream = false;
mSourceStoppedSignalled = false;
+ mMediaCodecEosSignalled = false;
mPendingCodecStop = false;
if (mStopTimeoutFuture != null) {
mStopTimeoutFuture.cancel(true);
mStopTimeoutFuture = null;
}
- mMediaCodecCallback = new MediaCodecCallback();
- mMediaCodec.setCallback(mMediaCodecCallback);
+ mMediaCodec.setCallback(new MediaCodecCallback());
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (mEncoderInput instanceof SurfaceInput) {
@@ -516,15 +515,13 @@
Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
mEncoderExecutor);
} else if (mEncoderInput instanceof SurfaceInput) {
- if (DeviceQuirks.get(MediaCodecDoesNotSendEos.class) != null) {
- requireNonNull(mMediaCodecCallback).onOutputBufferAvailable(mMediaCodec,
- FAKE_BUFFER_INDEX, createFakeEosBufferInfo());
- } else {
- try {
- mMediaCodec.signalEndOfInputStream();
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
- }
+ try {
+ mMediaCodec.signalEndOfInputStream();
+ // On some devices, MediaCodec#signalEndOfInputStream() doesn't work.
+ // See b/255209101.
+ mMediaCodecEosSignalled = true;
+ } catch (MediaCodec.CodecException e) {
+ handleEncodeError(e);
}
}
}
@@ -891,7 +888,7 @@
@SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
- long getAdjustedTimeUs(@NonNull MediaCodec.BufferInfo bufferInfo) {
+ long getAdjustedTimeUs(@NonNull BufferInfo bufferInfo) {
long adjustedTimeUs;
if (mTotalPausedDurationUs > 0L) {
adjustedTimeUs = bufferInfo.presentationTimeUs - mTotalPausedDurationUs;
@@ -953,13 +950,6 @@
}
}
- @NonNull
- private MediaCodec.BufferInfo createFakeEosBufferInfo() {
- MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
- bufferInfo.set(0, 0, generatePresentationTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
- return bufferInfo;
- }
-
@SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void matchAcquisitionsAndFreeBufferIndexes() {
@@ -997,12 +987,12 @@
}
@SuppressWarnings("WeakerAccess") // synthetic accessor
- static boolean isKeyFrame(@NonNull MediaCodec.BufferInfo bufferInfo) {
+ static boolean isKeyFrame(@NonNull BufferInfo bufferInfo) {
return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
}
@SuppressWarnings("WeakerAccess") // synthetic accessor
- static boolean isEndOfStream(@NonNull MediaCodec.BufferInfo bufferInfo) {
+ static boolean hasEndOfStreamFlag(@NonNull BufferInfo bufferInfo) {
return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
}
@@ -1021,7 +1011,7 @@
* The last sent presentation time of BufferInfo. The value could be adjusted by total
* pause duration.
*/
- private long mLastSentPresentationTimeUs = 0L;
+ private long mLastSentAdjustedTimeUs = 0L;
private boolean mIsOutputBufferInPauseState = false;
private boolean mIsKeyFrameRequired = false;
@@ -1065,7 +1055,7 @@
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, int index,
- @NonNull MediaCodec.BufferInfo bufferInfo) {
+ @NonNull BufferInfo bufferInfo) {
mEncoderExecutor.execute(() -> {
switch (mState) {
case STARTED:
@@ -1099,22 +1089,11 @@
if (!mHasFirstData) {
mHasFirstData = true;
}
- long adjustedTimeUs = getAdjustedTimeUs(bufferInfo);
- if (bufferInfo.presentationTimeUs != adjustedTimeUs) {
- // If adjusted time <= last sent time, the buffer should have been
- // detected and dropped in checkBufferInfo().
- Preconditions.checkState(
- adjustedTimeUs > mLastSentPresentationTimeUs);
- bufferInfo.presentationTimeUs = adjustedTimeUs;
- if (DEBUG) {
- Logger.d(mTag, "Adjust bufferInfo.presentationTimeUs to "
- + DebugUtils.readableUs(adjustedTimeUs));
- }
- }
- mLastSentPresentationTimeUs = bufferInfo.presentationTimeUs;
+ BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo);
+ mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;
try {
EncodedDataImpl encodedData = new EncodedDataImpl(mediaCodec, index,
- bufferInfo);
+ outBufferInfo);
sendEncodedData(encodedData, encoderCallback, executor);
} catch (MediaCodec.CodecException e) {
handleEncodeError(e);
@@ -1160,6 +1139,26 @@
}
@ExecutedBy("mEncoderExecutor")
+ @NonNull
+ private BufferInfo resolveOutputBufferInfo(@NonNull BufferInfo bufferInfo) {
+ long adjustedTimeUs = getAdjustedTimeUs(bufferInfo);
+ if (bufferInfo.presentationTimeUs == adjustedTimeUs) {
+ return bufferInfo;
+ }
+
+ // If adjusted time <= last sent time, the buffer should have been detected and
+ // dropped in checkBufferInfo().
+ Preconditions.checkState(adjustedTimeUs > mLastSentAdjustedTimeUs);
+ if (DEBUG) {
+ Logger.d(mTag, "Adjust bufferInfo.presentationTimeUs to "
+ + DebugUtils.readableUs(adjustedTimeUs));
+ }
+ BufferInfo newBufferInfo = new BufferInfo();
+ newBufferInfo.set(bufferInfo.offset, bufferInfo.size, adjustedTimeUs, bufferInfo.flags);
+ return newBufferInfo;
+ }
+
+ @ExecutedBy("mEncoderExecutor")
private void sendEncodedData(@NonNull EncodedDataImpl encodedData,
@NonNull EncoderCallback callback, @NonNull Executor executor) {
mEncodedDataSet.add(encodedData);
@@ -1191,12 +1190,12 @@
}
/**
- * Checks the {@link android.media.MediaCodec.BufferInfo} and updates related states.
+ * Checks the {@link BufferInfo} and updates related states.
*
* @return {@code true} if the buffer is valid, otherwise {@code false}.
*/
@ExecutedBy("mEncoderExecutor")
- private boolean checkBufferInfo(@NonNull MediaCodec.BufferInfo bufferInfo) {
+ private boolean checkBufferInfo(@NonNull BufferInfo bufferInfo) {
if (mHasEndData) {
Logger.d(mTag, "Drop buffer by already reach end of stream.");
return false;
@@ -1251,7 +1250,7 @@
}
// We should check if the adjusted time is valid. see b/189114207.
- if (getAdjustedTimeUs(bufferInfo) <= mLastSentPresentationTimeUs) {
+ if (getAdjustedTimeUs(bufferInfo) <= mLastSentAdjustedTimeUs) {
Logger.d(mTag, "Drop buffer by adjusted time is less than the last sent time.");
if (mIsVideoEncoder && isKeyFrame(bufferInfo)) {
mIsKeyFrameRequired = true;
@@ -1275,10 +1274,21 @@
return true;
}
+ @ExecutedBy("mEncoderExecutor")
+ private boolean isEndOfStream(@NonNull BufferInfo bufferInfo) {
+ return hasEndOfStreamFlag(bufferInfo) || isEosSignalledAndStopTimeReached(bufferInfo);
+ }
+
+ @ExecutedBy("mEncoderExecutor")
+ private boolean isEosSignalledAndStopTimeReached(@NonNull BufferInfo bufferInfo) {
+ return mMediaCodecEosSignalled
+ && bufferInfo.presentationTimeUs > mStartStopTimeRangeUs.getUpper();
+ }
+
@SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
@ExecutedBy("mEncoderExecutor")
private boolean updatePauseRangeStateAndCheckIfBufferPaused(
- @NonNull MediaCodec.BufferInfo bufferInfo) {
+ @NonNull BufferInfo bufferInfo) {
updateTotalPausedDuration(bufferInfo.presentationTimeUs);
boolean isInPauseRange = isInPauseRange(bufferInfo.presentationTimeUs);
if (!mIsOutputBufferInPauseState && isInPauseRange) {
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index d342afa..c553bc3 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -1072,7 +1072,7 @@
val surfaceRequest = SurfaceRequest(
DEFAULT_SURFACE_SIZE, fakeCamera,
isRGBA8888Required
- )
+ ) {}
surfaceRequestList.add(surfaceRequest)
return surfaceRequest
}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
index 0401800..3e0fede 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/SurfaceViewImplementationTest.kt
@@ -68,7 +68,7 @@
mParent = FrameLayout(mContext)
setContentView(mParent)
- mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+ mSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
mImplementation = SurfaceViewImplementation(mParent, PreviewTransformation())
}
@@ -92,7 +92,7 @@
val previousSurfaceView = mImplementation.mSurfaceView
// Act.
- val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+ val sameResolutionSurfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
mImplementation.testSurfaceRequest(sameResolutionSurfaceRequest)
val newSurfaceView = mImplementation.mSurfaceView
@@ -109,7 +109,8 @@
// Act.
val differentSize: Size by lazy { Size(720, 480) }
- val differentResolutionSurfaceRequest = SurfaceRequest(differentSize, FakeCamera(), false)
+ val differentResolutionSurfaceRequest =
+ SurfaceRequest(differentSize, FakeCamera(), false) {}
mImplementation.testSurfaceRequest(differentResolutionSurfaceRequest)
val newSurfaceView = mImplementation.mSurfaceView
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
index f69e55e..c6998bd 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/TextureViewImplementationTest.kt
@@ -50,7 +50,7 @@
private val surfaceRequest: SurfaceRequest
get() {
if (_surfaceRequest == null) {
- _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false)
+ _surfaceRequest = SurfaceRequest(ANY_SIZE, FakeCamera(), false) {}
}
return _surfaceRequest!!
}
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 8963ecc..1d141c6 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
@@ -1006,8 +1006,10 @@
if (mImplementation instanceof TextureViewImplementation) {
matrix.postConcat(getMatrix());
} else {
- Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
- + " to work correctly.");
+ if (!getMatrix().isIdentity()) {
+ Logger.w(TAG, "PreviewView needs to be in COMPATIBLE mode for the transform"
+ + " to work correctly.");
+ }
}
return new OutputTransform(matrix, new Size(surfaceCropRect.width(),
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java b/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
index f2d3255..dcd4fec 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/SurfaceViewImplementation.java
@@ -77,6 +77,12 @@
ContextCompat.getMainExecutor(mSurfaceView.getContext()),
onSurfaceNotInUseListener::onSurfaceNotInUse);
}
+
+ // Note that View.post will add the Runnable to SurfaceView's message queue. This means
+ // that if this line is called while the SurfaceView is detached from window,
+ // "setSurfaceRequest" will be pending til the SurfaceView is attached to window and its
+ // view is prepared. In other words, "setSurfaceRequest" will happen after
+ // "surfaceCreated" is triggered.
mSurfaceView.post(() -> mSurfaceRequestCallback.setSurfaceRequest(surfaceRequest,
onSurfaceNotInUseListener));
}
@@ -161,6 +167,9 @@
private SurfaceRequest mSurfaceRequest;
@Nullable
+ private SurfaceRequest mSurfaceRequestToBeInvalidated;
+
+ @Nullable
private OnSurfaceNotInUseListener mOnSurfaceNotInUseListener;
// The cached size of the current Surface.
@@ -171,6 +180,8 @@
// Guarded by the UI thread.
private boolean mWasSurfaceProvided = false;
+ private boolean mNeedToInvalidate = false;
+
/**
* Sets the completer and the size. The completer will only be set if the current size of
* the Surface matches the target size.
@@ -181,17 +192,29 @@
// Cancel the previous request, if any
cancelPreviousRequest();
- mSurfaceRequest = surfaceRequest;
- mOnSurfaceNotInUseListener = onSurfaceNotInUseListener;
- Size targetSize = surfaceRequest.getResolution();
- mTargetSize = targetSize;
- mWasSurfaceProvided = false;
+ if (mNeedToInvalidate) {
+ // In some edge cases, the DeferrableSurface behind the SurfaceRequest is timed-out.
+ // Since we can not tell if the timeout happened, we invalidate the
+ // SurfaceRequest to get a new one when the situation is abnormal. (Normally,
+ // invalidate is called when the surface is recreated.)
+ // It's not ideal to track the "timed out" state of the SurfaceRequest this way.
+ // A better way would be making it part of SurfaceRequest. e.g. something like
+ // SurfaceRequest.isTimedOut().
+ mNeedToInvalidate = false;
+ surfaceRequest.invalidate();
+ } else {
+ mSurfaceRequest = surfaceRequest;
+ mOnSurfaceNotInUseListener = onSurfaceNotInUseListener;
+ Size targetSize = surfaceRequest.getResolution();
+ mTargetSize = targetSize;
+ mWasSurfaceProvided = false;
- if (!tryToComplete()) {
- // The current size is incorrect. Wait for it to change.
- Logger.d(TAG, "Wait for new Surface creation.");
- mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
- targetSize.getHeight());
+ if (!tryToComplete()) {
+ // The current size is incorrect. Wait for it to change.
+ Logger.d(TAG, "Wait for new Surface creation.");
+ mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
+ targetSize.getHeight());
+ }
}
}
@@ -239,9 +262,9 @@
@UiThread
@SuppressWarnings("ObjectToString")
- private void invalidateSurface() {
+ private void closeSurface() {
if (mSurfaceRequest != null) {
- Logger.d(TAG, "Surface invalidated " + mSurfaceRequest);
+ Logger.d(TAG, "Surface closed " + mSurfaceRequest);
mSurfaceRequest.getDeferrableSurface().close();
}
}
@@ -249,7 +272,14 @@
@Override
public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) {
Logger.d(TAG, "Surface created.");
- // No-op. Handling surfaceChanged() is enough because it's always called afterwards.
+
+ // Invalidate the surface request so that the requester is notified that the previously
+ // obtained surface is no longer valid and should request a new one.
+ if (mNeedToInvalidate && mSurfaceRequestToBeInvalidated != null) {
+ mSurfaceRequestToBeInvalidated.invalidate();
+ mSurfaceRequestToBeInvalidated = null;
+ mNeedToInvalidate = false;
+ }
}
@Override
@@ -264,14 +294,21 @@
public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) {
Logger.d(TAG, "Surface destroyed.");
- // If a surface was already provided to the camera, invalidate it so that it requests
- // a new valid one. Otherwise, cancel the surface request.
+ // If a surface was already provided to the camera, close the surface. Otherwise,
+ // cancel the surface request.
if (mWasSurfaceProvided) {
- invalidateSurface();
+ closeSurface();
} else {
cancelPreviousRequest();
}
+ // The surface is no longer valid. The surface request will be invalidated when the new
+ // surface is ready so that the requester can get the new one.
+ mNeedToInvalidate = true;
+ if (mSurfaceRequest != null) {
+ mSurfaceRequestToBeInvalidated = mSurfaceRequest;
+ }
+
// Reset state
mWasSurfaceProvided = false;
mSurfaceRequest = null;
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
index 0ca07ad..804de8a 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewTest.java
@@ -140,6 +140,6 @@
cameraInfoInternal.setImplementationType(CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
return new SurfaceRequest(new Size(800, 600),
new FakeCamera(null, cameraInfoInternal),
- /*isRGB8888Required*/ false);
+ /*isRGB8888Required*/ false, () -> {});
}
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
index 9f5d0e3..4930577 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/viewpager/ViewPager2ActivityTest.kt
@@ -25,6 +25,8 @@
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.CameraSelector
import androidx.camera.integration.uiwidgets.R
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.COMPATIBLE_MODE
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.PERFORMANCE_MODE
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
@@ -57,27 +59,37 @@
@RunWith(Parameterized::class)
@LargeTest
-class ViewPager2ActivityTest(private val lensFacing: Int, private val cameraXConfig: String) {
+class ViewPager2ActivityTest(
+ private val lensFacing: Int,
+ private val implementationMode: Int,
+ private val cameraXConfig: String
+) {
companion object {
private const val ACTION_IDLE_TIMEOUT: Long = 5000
+ private const val PREVIEW_UPDATE_COUNT = 10
@JvmStatic
private val lensFacingList =
arrayOf(CameraSelector.LENS_FACING_BACK, CameraSelector.LENS_FACING_FRONT)
@JvmStatic
+ private val implementationModeList = arrayOf(COMPATIBLE_MODE, PERFORMANCE_MODE)
+
+ @JvmStatic
private val cameraXConfigList = arrayOf(
CameraFragment.CAMERA2_IMPLEMENTATION_OPTION,
CameraFragment.CAMERA_PIPE_IMPLEMENTATION_OPTION
)
@JvmStatic
- @Parameterized.Parameters(name = "lensFacing={0}, cameraXConfig={1}")
+ @Parameterized.Parameters(name = "lensFacing={0}, mode={1}, cameraXConfig={2}")
fun data() = mutableListOf<Array<Any?>>().apply {
lensFacingList.forEach { lens ->
- cameraXConfigList.forEach { cameraXConfig ->
- add(arrayOf(lens, cameraXConfig))
+ implementationModeList.forEach { mode ->
+ cameraXConfigList.forEach { cameraXConfig ->
+ add(arrayOf(lens, mode, cameraXConfig))
+ }
}
}
}
@@ -139,7 +151,7 @@
assertStreamState(scenario, PreviewView.StreamState.STREAMING)
// Make sure the surface texture of TextureView continues getting updates.
- assertSurfaceTextureFramesUpdate(scenario)
+ assertPreviewViewUpdate(scenario)
}
}
@@ -161,7 +173,7 @@
// For b/149877652, need to check if the surface texture of TextureView continues
// getting updates after detaching from window and then attaching to window.
- assertSurfaceTextureFramesUpdate(scenario)
+ assertPreviewViewUpdate(scenario)
}
}
@@ -188,7 +200,7 @@
// The test covers pause/resume and ViewPager2 switch behaviors. Hence, need to
// check the surface texture of TextureView continues getting updates for b/149877652.
- assertSurfaceTextureFramesUpdate(scenario)
+ assertPreviewViewUpdate(scenario)
}
}
@@ -198,14 +210,15 @@
):
ActivityScenario<ViewPager2Activity> {
val intent = Intent(
- ApplicationProvider.getApplicationContext<Context>(),
+ ApplicationProvider.getApplicationContext(),
ViewPager2Activity::class.java
).apply {
putExtra(BaseActivity.INTENT_LENS_FACING, lensFacing)
+ putExtra(BaseActivity.INTENT_IMPLEMENTATION_MODE, implementationMode)
putExtra(CameraFragment.KEY_CAMERA_IMPLEMENTATION, cameraXConfig)
putExtra(CameraFragment.KEY_CAMERA_IMPLEMENTATION_NO_HISTORY, true)
}
- return ActivityScenario.launch<ViewPager2Activity>(intent)
+ return ActivityScenario.launch(intent)
}
private fun getTextureView(previewView: PreviewView): TextureView? {
@@ -238,6 +251,14 @@
assertThat(result.await()).isTrue()
}
+ private fun assertPreviewViewUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
+ when (implementationMode) {
+ COMPATIBLE_MODE -> assertSurfaceTextureFramesUpdate(scenario)
+ PERFORMANCE_MODE -> assertPreviewUpdate(scenario)
+ else -> throw IllegalArgumentException()
+ }
+ }
+
private fun assertSurfaceTextureFramesUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
var newSurfaceTexture: SurfaceTexture? = null
lateinit var previewView: PreviewView
@@ -253,4 +274,24 @@
}
assertThat(latchForFrameUpdate.await(ACTION_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
}
+
+ /**
+ * Checks update from Preview instead of SurfaceView, since the SurfaceView's content can not
+ * be got.
+ */
+ private fun assertPreviewUpdate(scenario: ActivityScenario<ViewPager2Activity>) {
+ val latch = CountDownLatch(PREVIEW_UPDATE_COUNT)
+ getCameraFragment(scenario)?.setPreviewUpdatingLatch(latch)
+ assertThat(latch.await(ACTION_IDLE_TIMEOUT, TimeUnit.MILLISECONDS)).isTrue()
+ }
+
+ private fun getCameraFragment(scenario: ActivityScenario<ViewPager2Activity>): CameraFragment? {
+ var fragment: CameraFragment? = null
+ scenario.onActivity { activity ->
+ val fragments = activity.supportFragmentManager.fragments
+ fragment = fragments.firstOrNull { it is CameraFragment } as? CameraFragment
+ }
+
+ return fragment
+ }
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
index 32047a0..2a54dfc 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/BaseActivity.kt
@@ -31,6 +31,9 @@
private const val TAG = "BasicActivity"
private const val LATCH_TIMEOUT: Long = 5000
internal const val INTENT_LENS_FACING = "lens-facing"
+ internal const val INTENT_IMPLEMENTATION_MODE = "implementation-mode"
+ internal const val PERFORMANCE_MODE = 0
+ internal const val COMPATIBLE_MODE = 1
}
// The expected final streamState
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
index 4e6ae5b..964d3d8 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/viewpager/CameraFragment.kt
@@ -18,6 +18,9 @@
import android.content.Context
import android.content.Intent
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.TotalCaptureResult
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
@@ -25,18 +28,24 @@
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
+import androidx.annotation.VisibleForTesting
import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop
import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview
import androidx.camera.integration.uiwidgets.databinding.FragmentTextureviewBinding
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.COMPATIBLE_MODE
+import androidx.camera.integration.uiwidgets.viewpager.BaseActivity.Companion.PERFORMANCE_MODE
import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
import androidx.camera.lifecycle.ProcessCameraProvider
-import androidx.camera.view.PreviewView
+import androidx.camera.view.PreviewView.ImplementationMode
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.CountDownLatch
/** A Fragment that displays a {@link PreviewView} with TextureView mode. */
class CameraFragment : Fragment() {
@@ -57,6 +66,9 @@
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private lateinit var cameraProvider: ProcessCameraProvider
+ // for testing preview updates
+ private var previewUpdatingLatch: CountDownLatch? = null
+
@OptIn(ExperimentalCameraProviderConfiguration::class)
override fun onAttach(context: Context) {
super.onAttach(context)
@@ -128,13 +140,15 @@
private fun bindPreview() {
Log.d(TAG, "bindPreview")
- val preview = Preview.Builder()
+ val previewBuilder = Preview.Builder()
+ previewBuilder.addCaptureCompletedCallback()
+ val preview = previewBuilder
.setTargetName("Preview")
.build()
cameraProvider.bindToLifecycle(this, getCameraSelector(), preview)
- binding.previewTextureview.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
+ binding.previewTextureview.implementationMode = getImplementationMode()
preview.setSurfaceProvider(binding.previewTextureview.surfaceProvider)
}
@@ -148,4 +162,46 @@
.requireLensFacing(lensFacing)
.build()
}
+
+ /**
+ * Returns the implementation mode from the intent, or return the compatibility mode if not set.
+ */
+ private fun getImplementationMode(): ImplementationMode {
+ val mode = (requireActivity() as BaseActivity).intent.getIntExtra(
+ BaseActivity.INTENT_IMPLEMENTATION_MODE, COMPATIBLE_MODE
+ )
+
+ return when (mode) {
+ PERFORMANCE_MODE -> ImplementationMode.PERFORMANCE
+ else -> ImplementationMode.COMPATIBLE
+ }
+ }
+
+ /**
+ * Implements preview updating latch with interop to workaround the situation that SurfaceView's
+ * content can not be got.
+ */
+ @OptIn(ExperimentalCamera2Interop::class)
+ private fun Preview.Builder.addCaptureCompletedCallback() {
+ val captureCallback = object : CameraCaptureSession.CaptureCallback() {
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ super.onCaptureCompleted(session, request, result)
+
+ if (previewUpdatingLatch != null) {
+ previewUpdatingLatch!!.countDown()
+ }
+ }
+ }
+
+ Camera2Interop.Extender(this).setSessionCaptureCallback(captureCallback)
+ }
+
+ @VisibleForTesting
+ fun setPreviewUpdatingLatch(latch: CountDownLatch) {
+ previewUpdatingLatch = latch
+ }
}
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
index eeefe36..b451e89 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/info/AutomotiveCarInfo.java
@@ -35,6 +35,7 @@
import static android.car.VehiclePropertyIds.RANGE_REMAINING;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.hardware.common.CarValue.STATUS_UNKNOWN;
import static androidx.car.app.hardware.common.CarValueUtils.getCarValue;
import static java.util.Objects.requireNonNull;
@@ -97,7 +98,6 @@
// VEHICLE_SPEED_DISPLAY_UNIT in VehiclePropertyIds. The property is added after Android Q.
public static final int SPEED_DISPLAY_UNIT_ID = 289408516;
- private static final float UNKNOWN_CAPACITY = Float.NEGATIVE_INFINITY;
static final ImmutableMap<Integer, List<CarZone>> ENERGY_LEVEL_REQUEST = ImmutableMap.<Integer,
List<CarZone>>builder()
.put(EV_BATTERY_LEVEL, GLOBAL_CAR_ZONE)
@@ -284,14 +284,12 @@
return;
}
for (CarPropertyResponse<?> carPropertyResponse : carPropertyResponses) {
- if (carPropertyResponse.getPropertyId() == INFO_EV_BATTERY_CAPACITY
- && carPropertyResponse.getValue() != null) {
- energyLevelListener.updateEvBatteryCapacity(
- (Float) carPropertyResponse.getValue());
+ if (carPropertyResponse.getPropertyId() == INFO_EV_BATTERY_CAPACITY) {
+ energyLevelListener.updateEvBatteryCapacityPropertyResponse(
+ carPropertyResponse);
}
- if (carPropertyResponse.getPropertyId() == INFO_FUEL_CAPACITY
- && carPropertyResponse.getValue() != null) {
- energyLevelListener.updateFuelCapacity((Float) carPropertyResponse.getValue());
+ if (carPropertyResponse.getPropertyId() == INFO_FUEL_CAPACITY) {
+ energyLevelListener.updateFuelCapacityPropertyResponse(carPropertyResponse);
}
}
}, executor);
@@ -563,20 +561,25 @@
private final OnCarDataAvailableListener<EnergyLevel>
mEnergyLevelOnCarDataAvailableListener;
private final Executor mExecutor;
- private float mEvBatteryCapacity = UNKNOWN_CAPACITY;
- private float mFuelCapacity = UNKNOWN_CAPACITY;
+ private CarPropertyResponse<?> mEvBatteryCapacityPropertyResponse = CarPropertyResponse
+ .builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_UNKNOWN)
+ .build();
+ private CarPropertyResponse<?> mFuelCapacityPropertyResponse = CarPropertyResponse.builder()
+ .setPropertyId(INFO_FUEL_CAPACITY).setStatus(STATUS_UNKNOWN).build();
EnergyLevelListener(OnCarDataAvailableListener<EnergyLevel> listener, Executor executor) {
mEnergyLevelOnCarDataAvailableListener = listener;
mExecutor = executor;
}
- void updateEvBatteryCapacity(float evBatteryCapacity) {
- mEvBatteryCapacity = evBatteryCapacity;
+ void updateEvBatteryCapacityPropertyResponse(
+ CarPropertyResponse<?> evBatteryCapacityPropertyResponse) {
+ mEvBatteryCapacityPropertyResponse = evBatteryCapacityPropertyResponse;
}
- void updateFuelCapacity(float fuelCapacity) {
- mFuelCapacity = fuelCapacity;
+ void updateFuelCapacityPropertyResponse(
+ CarPropertyResponse<?> fuelCapacityPropertyResponse) {
+ mFuelCapacityPropertyResponse = fuelCapacityPropertyResponse;
}
// TODO(b/202303614): Remove this annotation once FuelVolumeDisplayUnit is ready.
@@ -589,29 +592,27 @@
for (CarPropertyResponse<?> response : carPropertyResponses) {
switch (response.getPropertyId()) {
case EV_BATTERY_LEVEL:
- if (mEvBatteryCapacity == UNKNOWN_CAPACITY) {
- Log.w(LogTags.TAG_CAR_HARDWARE, "EV battery capacity is still "
- + "unknown, skipping EV_BATTERY_LEVEL update");
- continue;
- }
- if (response.getValue() != null) {
- energyLevelBuilder.setBatteryPercent(getCarValue(response,
- (Float) response.getValue() / mEvBatteryCapacity * 100));
- } else {
+ if (response.getValue() == null) {
energyLevelBuilder.setBatteryPercent(getCarValue(response));
+ } else if (mEvBatteryCapacityPropertyResponse.getValue() == null) {
+ energyLevelBuilder.setBatteryPercent(
+ getCarValue(mEvBatteryCapacityPropertyResponse));
+ } else {
+ energyLevelBuilder.setBatteryPercent(getCarValue(response,
+ (Float) response.getValue() / (Float)
+ mEvBatteryCapacityPropertyResponse.getValue() * 100));
}
break;
case FUEL_LEVEL:
- if (mFuelCapacity == UNKNOWN_CAPACITY) {
- Log.w(LogTags.TAG_CAR_HARDWARE, "Fuel capacity is still unknown, "
- + "skipping FUEL_LEVEL update");
- continue;
- }
- if (response.getValue() != null) {
- energyLevelBuilder.setFuelPercent(getCarValue(response,
- (Float) response.getValue() / mFuelCapacity * 100));
- } else {
+ if (response.getValue() == null) {
energyLevelBuilder.setFuelPercent(getCarValue(response));
+ } else if (mFuelCapacityPropertyResponse.getValue() == null) {
+ energyLevelBuilder.setFuelPercent(
+ getCarValue(mFuelCapacityPropertyResponse));
+ } else {
+ energyLevelBuilder.setFuelPercent(getCarValue(response,
+ (Float) response.getValue() / (Float)
+ mFuelCapacityPropertyResponse.getValue() * 100));
}
break;
case FUEL_LEVEL_LOW:
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
index e6818f2..55a1dba 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/info/AutomotiveCarInfoTest.java
@@ -36,6 +36,7 @@
import static androidx.car.app.hardware.common.CarValue.STATUS_SUCCESS;
import static androidx.car.app.hardware.common.CarValue.STATUS_UNAVAILABLE;
+import static androidx.car.app.hardware.common.CarValue.STATUS_UNIMPLEMENTED;
import static androidx.car.app.hardware.common.CarValue.STATUS_UNKNOWN;
import static androidx.car.app.hardware.info.AutomotiveCarInfo.DEFAULT_SAMPLE_RATE;
import static androidx.car.app.hardware.info.AutomotiveCarInfo.SPEED_DISPLAY_UNIT_ID;
@@ -118,6 +119,62 @@
private static final List<CarZone> GLOBAL_ZONE = Collections.singletonList(
CarZone.CAR_ZONE_GLOBAL);
+ private static final int METER_DISTANCE_UNIT = 0x21;
+ private static final int METER_VOLUME_UNIT = 0x40;
+ private static final float EV_BATTERY_CAPACITY = 100f;
+ private static final float EV_BATTERY_LEVEL_VALUE = 50f;
+ private static final float FUEL_CAPACITY = 120f;
+ private static final float FUEL_LEVEL_VALUE = 50f;
+ private static final long DEFAULT_TIMESTAMP_MILLIS = 1L;
+ private static final boolean FUEL_LEVEL_LOW_VALUE = true;
+ private static final float RANGE_REMAINING_VALUE = 5f;
+ private static final CarPropertyResponse<?> EV_BATTERY_CAPACITY_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+ STATUS_SUCCESS).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).setValue(
+ EV_BATTERY_CAPACITY).build();
+ private static final CarPropertyResponse<?> FUEL_CAPACITY_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
+ STATUS_SUCCESS).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).setValue(
+ FUEL_CAPACITY).build();
+ private static final CarPropertyResponse<?> EV_BATTERY_LEVEL_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
+ STATUS_SUCCESS).setValue(EV_BATTERY_LEVEL_VALUE).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarPropertyResponse<?> FUEL_LEVEL_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
+ STATUS_SUCCESS).setValue(FUEL_LEVEL_VALUE).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarPropertyResponse<?> FUEL_LEVEL_LOW_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
+ STATUS_SUCCESS).setValue(FUEL_LEVEL_LOW_VALUE).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarPropertyResponse<?> RANGE_REMAINING_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
+ STATUS_SUCCESS).setValue(RANGE_REMAINING_VALUE).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarPropertyResponse<?> DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
+ STATUS_SUCCESS).setValue(METER_DISTANCE_UNIT).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarPropertyResponse<?> FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS =
+ CarPropertyResponse.builder().setPropertyId(FUEL_VOLUME_DISPLAY_UNITS).setStatus(
+ STATUS_SUCCESS).setValue(METER_VOLUME_UNIT).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build();
+ private static final CarValue<Float> EXPECTED_BATTERY_PERCENT_SUCCESS =
+ new CarValue<>(EV_BATTERY_LEVEL_VALUE / EV_BATTERY_CAPACITY * 100,
+ DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+ private static final CarValue<Float> EXPECTED_FUEL_PERCENT_SUCCESS =
+ new CarValue<>(FUEL_LEVEL_VALUE / FUEL_CAPACITY * 100, DEFAULT_TIMESTAMP_MILLIS,
+ STATUS_SUCCESS);
+ private static final CarValue<Boolean> EXPECTED_ENERGY_IS_LOW_SUCCESS =
+ new CarValue<>(FUEL_LEVEL_LOW_VALUE, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+ private static final CarValue<Float> EXPECTED_RANGE_REMAINING_METERS_SUCCESS =
+ new CarValue<>(RANGE_REMAINING_VALUE, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+ private static final CarValue<Integer> EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS =
+ new CarValue<>(2, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+ private static final CarValue<Integer> EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS =
+ new CarValue<>(201, DEFAULT_TIMESTAMP_MILLIS, STATUS_SUCCESS);
+
@Before
public void setUp() {
ShadowCar.setCar(mCarMock);
@@ -857,27 +914,15 @@
assertThat(loadedResult.get()).isEqualTo(new Speed.Builder().build());
}
- @Test
- public void getEnergyLevel_verifyResponse() throws InterruptedException {
+ private void getEnergyLevelHelperFunction(List<CarPropertyResponse<?>> energyCapacities,
+ List<CarPropertyResponse<?>> energyResponses, EnergyLevel expectedEnergyLevel) throws
+ InterruptedException {
// Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
- ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
- OnCarPropertyResponseListener.class);
- int meterDistanceUnit = 0x21;
- int meterVolumeUnit = 0x40;
- float evBatteryCapacity = 100f;
- float evBatteryLevelValue = 50f;
- float fuelCapacity = 120f;
- float fuelLevelValue = 50f;
- List<CarPropertyResponse<?>> capacities = new ArrayList<>();
- capacities.add(CarPropertyResponse.builder().setPropertyId(
- INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
- evBatteryCapacity).setTimestampMillis(1L).build());
- capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
- STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
- ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
+ ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(
+ energyCapacities);
when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
future);
@@ -901,309 +946,273 @@
.put(DISTANCE_DISPLAY_UNITS, mCarZones)
.put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
.buildKeepingLast();
+ ArgumentCaptor<OnCarPropertyResponseListener> listenerCaptor = ArgumentCaptor.forClass(
+ OnCarPropertyResponseListener.class);
- verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
- eq(mExecutor));
- verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
- eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
- eq(mExecutor));
+ verify(mPropertyManager).submitGetPropertyRequest(mGetPropertyRequests, mExecutor);
+ verify(mPropertyManager).submitRegisterListenerRequest(eq(propertyIdsWithCarZones),
+ eq(DEFAULT_SAMPLE_RATE), listenerCaptor.capture(), eq(mExecutor));
- mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
- STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
- STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
- STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(
- FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_SUCCESS).setValue(
- meterVolumeUnit).setTimestampMillis(1L).build());
+ mResponse.addAll(energyResponses);
- captor.getValue().onCarPropertyResponses(mResponse);
+ listenerCaptor.getValue().onCarPropertyResponses(mResponse);
mCountDownLatch.await();
- EnergyLevel energyLevel = loadedResult.get();
- assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
- evBatteryLevelValue / evBatteryCapacity * 100);
- assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
- fuelLevelValue / fuelCapacity * 100);
- assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
- assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
- assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
- assertThat(energyLevel.getFuelVolumeDisplayUnit().getValue()).isEqualTo(201);
+ assertThat(loadedResult.get()).isEqualTo(expectedEnergyLevel);
+ }
+
+ @Test
+ public void getEnergyLevel_verifyResponse() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+ EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
}
@Test
public void addEnergyLevelListener_handlesReponsesWithDifferentStatuses()
throws InterruptedException {
- // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
- ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
- OnCarPropertyResponseListener.class);
- int meterDistanceUnit = 0x21;
- float evBatteryCapacity = 100f;
- float fuelCapacity = 120f;
- List<CarPropertyResponse<?>> capacities = new ArrayList<>();
- capacities.add(CarPropertyResponse.builder().setPropertyId(
- INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
- evBatteryCapacity).setTimestampMillis(1L).build());
- capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
- STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
- ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
- when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
- future);
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
+ STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
+ STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS,
+ CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
+ STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ CarPropertyResponse.builder().setPropertyId(FUEL_VOLUME_DISPLAY_UNITS).setStatus(
+ STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build());
- AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
- OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
- loadedResult.set(data);
- mCountDownLatch.countDown();
- };
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setFuelPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+ STATUS_UNKNOWN)).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).build();
- mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
-
- // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
- // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
- // zones.
- Map<Integer, List<CarZone>> propertyIdsWithCarZones =
- ImmutableMap.<Integer, List<CarZone>>builder().put(EV_BATTERY_LEVEL, mCarZones).put(
- FUEL_LEVEL, mCarZones).put(FUEL_LEVEL_LOW, mCarZones).put(RANGE_REMAINING,
- mCarZones).put(DISTANCE_DISPLAY_UNITS, mCarZones).put(
- FUEL_VOLUME_DISPLAY_UNITS, mCarZones).buildKeepingLast();
-
- verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
- eq(mExecutor));
- verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
- eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
- eq(mExecutor));
-
- mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
- STATUS_UNKNOWN).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
- STATUS_UNAVAILABLE).setTimestampMillis(2L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
- STATUS_SUCCESS).setValue(true).setTimestampMillis(3L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
- STATUS_UNKNOWN).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
- STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(4L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(
- FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_UNAVAILABLE).setTimestampMillis(
- 6L).build());
-
- captor.getValue().onCarPropertyResponses(mResponse);
- mCountDownLatch.await();
-
- assertThat(loadedResult.get()).isEqualTo(new EnergyLevel.Builder().setBatteryPercent(
- new CarValue<>(null, 1, STATUS_UNKNOWN)).setFuelPercent(
- new CarValue<>(null, 2, STATUS_UNAVAILABLE)).setEnergyIsLow(
- new CarValue<>(true, 3, STATUS_SUCCESS)).setRangeRemainingMeters(
- new CarValue<>(null, 1, STATUS_UNKNOWN)).setFuelVolumeDisplayUnit(
- new CarValue<>(null, 6, STATUS_UNAVAILABLE)).setDistanceDisplayUnit(
- new CarValue<>(CarUnit.METER, 4, STATUS_SUCCESS)).build());
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
}
@Test
public void addEnergyLevelListener_returnsEnergyLevelWithUnknownValuesIfNoResponses()
throws InterruptedException {
- // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
- List<CarPropertyResponse<?>> capacities = new ArrayList<>();
- when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
- Futures.immediateFuture(capacities));
- AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
- OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
- loadedResult.set(data);
- mCountDownLatch.countDown();
- };
+ List<CarPropertyResponse<?>> energyCapacities = new ArrayList<>();
- mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
+ List<CarPropertyResponse<?>> energyResponses = new ArrayList<>();
- verify(mPropertyManager, times(1)).submitGetPropertyRequest(
- eq(mGetPropertyRequests), eq(mExecutor));
- // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
- // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
- // zones.
- Map<Integer, List<CarZone>> propertyIdsWithCarZones =
- ImmutableMap.<Integer, List<CarZone>>builder()
- .put(EV_BATTERY_LEVEL, mCarZones)
- .put(FUEL_LEVEL, mCarZones)
- .put(FUEL_LEVEL_LOW, mCarZones)
- .put(RANGE_REMAINING, mCarZones)
- .put(DISTANCE_DISPLAY_UNITS, mCarZones)
- .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
- .buildKeepingLast();
- ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
- OnCarPropertyResponseListener.class);
- verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
- eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
- eq(mExecutor));
- captor.getValue().onCarPropertyResponses(mResponse);
- mCountDownLatch.await();
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().build();
- assertThat(loadedResult.get()).isEqualTo(new EnergyLevel.Builder().build());
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
}
@Test
public void getEnergyLevel_withUnavailableCapacityValues() throws InterruptedException {
- // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+ STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
+ STATUS_UNAVAILABLE).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build());
- ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
- OnCarPropertyResponseListener.class);
- int meterDistanceUnit = 0x21;
- int meterVolumeUnit = 0x40;
- float evBatteryLevelValue = 50f;
- float fuelLevelValue = 50f;
- List<CarPropertyResponse<?>> capacities = new ArrayList<>();
- capacities.add(CarPropertyResponse.builder().setPropertyId(
- INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_UNAVAILABLE).setTimestampMillis(
- 1L).build());
- capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
- STATUS_UNAVAILABLE).setTimestampMillis(1L).build());
- ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
- when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
- future);
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
- AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
- OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
- loadedResult.set(data);
- mCountDownLatch.countDown();
- };
+ // Battery percent and fuel percent should be null since we can not get the capacity of
+ // battery and fuel property. The other properties should still work without capacity
+ // values.
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setFuelPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNAVAILABLE)).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
- mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
- // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
- // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
- // zones.
- Map<Integer, List<CarZone>> propertyIdsWithCarZones =
- ImmutableMap.<Integer, List<CarZone>>builder()
- .put(EV_BATTERY_LEVEL, mCarZones)
- .put(FUEL_LEVEL, mCarZones)
- .put(FUEL_LEVEL_LOW, mCarZones)
- .put(RANGE_REMAINING, mCarZones)
- .put(DISTANCE_DISPLAY_UNITS, mCarZones)
- .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
- .buildKeepingLast();
+ @Test
+ public void getEnergyLevel_withUnimplementedEvBatteryCapacity() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+ STATUS_UNIMPLEMENTED).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ FUEL_CAPACITY_RESPONSE_SUCCESS);
- verify(mPropertyManager, times(1)).submitGetPropertyRequest(
- eq(mGetPropertyRequests),
- eq(mExecutor));
- verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
- eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
- eq(mExecutor));
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
- mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
- STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
- STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
- STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(
- FUEL_VOLUME_DISPLAY_UNITS).setStatus(STATUS_SUCCESS).setValue(
- meterVolumeUnit).setTimestampMillis(1L).build());
- captor.getValue().onCarPropertyResponses(mResponse);
- mCountDownLatch.await();
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+ STATUS_UNIMPLEMENTED)).setFuelPercent(
+ EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
- EnergyLevel energyLevel = loadedResult.get();
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
- // Battery percent and fuel percent should be UNKNOWN_FLOAT since we can not get
- // the capacity of battery and fuel property.
- assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
- CarValue.UNKNOWN_FLOAT.getValue());
- assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
- CarValue.UNKNOWN_FLOAT.getValue());
+ @Test
+ public void getEnergyLevel_withUnimplementedFuelCapacity() throws
+ InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, CarPropertyResponse.builder().setPropertyId(
+ INFO_FUEL_CAPACITY).setStatus(STATUS_UNIMPLEMENTED).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build());
- // The other properties should still work without capacity values
- assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
- assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
- assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
- assertThat(energyLevel.getFuelVolumeDisplayUnit().getValue()).isEqualTo(201);
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS,
+ STATUS_UNIMPLEMENTED)).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
+
+ @Test
+ public void getEnergyLevel_withUnknownEvBatteryCapacity() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ CarPropertyResponse.builder().setPropertyId(INFO_EV_BATTERY_CAPACITY).setStatus(
+ STATUS_UNKNOWN).setTimestampMillis(DEFAULT_TIMESTAMP_MILLIS).build(),
+ FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setFuelPercent(
+ EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
+
+ @Test
+ public void getEnergyLevel_withUnknownFuelCapacity() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, CarPropertyResponse.builder().setPropertyId(
+ INFO_FUEL_CAPACITY).setStatus(STATUS_UNKNOWN).setTimestampMillis(
+ DEFAULT_TIMESTAMP_MILLIS).build());
+
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+ new CarValue<>(null, DEFAULT_TIMESTAMP_MILLIS, STATUS_UNKNOWN)).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
+
+ @Test
+ public void getEnergyLevel_withNoEvBatteryCapacityResponse() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ FUEL_CAPACITY_RESPONSE_SUCCESS);
+
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ new CarValue<>(null, 0, STATUS_UNKNOWN)).setFuelPercent(
+ EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
+ }
+
+ @Test
+ public void getEnergyLevel_withNoFuelCapacityResponse() throws InterruptedException {
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS);
+
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS,
+ FUEL_VOLUME_DISPLAY_UNITS_RESPONSE_SUCCESS);
+
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+ new CarValue<>(null, 0, STATUS_UNKNOWN)).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).setFuelVolumeDisplayUnit(
+ EXPECTED_FUEL_VOLUME_DISPLAY_UNIT_SUCCESS).build();
+
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
}
@Test
public void getEnergyLevel_SuccessfulPartialResponses() throws InterruptedException {
- // Add "INFO_EV_BATTERY_CAPACITY" and "INFO_FUEL_CAPACITY" to the request.
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_EV_BATTERY_CAPACITY));
- mGetPropertyRequests.add(GetPropertyRequest.create(INFO_FUEL_CAPACITY));
+ List<CarPropertyResponse<?>> energyCapacities = Arrays.asList(
+ EV_BATTERY_CAPACITY_RESPONSE_SUCCESS, FUEL_CAPACITY_RESPONSE_SUCCESS);
- ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
- OnCarPropertyResponseListener.class);
- int meterDistanceUnit = 0x21;
- float evBatteryCapacity = 100f;
- float evBatteryLevelValue = 50f;
- float fuelCapacity = 120f;
- float fuelLevelValue = 50f;
- List<CarPropertyResponse<?>> capacities = new ArrayList<>();
- capacities.add(CarPropertyResponse.builder().setPropertyId(
- INFO_EV_BATTERY_CAPACITY).setStatus(STATUS_SUCCESS).setValue(
- evBatteryCapacity).setTimestampMillis(1L).build());
- capacities.add(CarPropertyResponse.builder().setPropertyId(INFO_FUEL_CAPACITY).setStatus(
- STATUS_SUCCESS).setValue(fuelCapacity).setTimestampMillis(1L).build());
- ListenableFuture<List<CarPropertyResponse<?>>> future = Futures.immediateFuture(capacities);
- when(mPropertyManager.submitGetPropertyRequest(eq(mGetPropertyRequests), any())).thenReturn(
- future);
+ List<CarPropertyResponse<?>> energyResponses = Arrays.asList(
+ EV_BATTERY_LEVEL_RESPONSE_SUCCESS, FUEL_LEVEL_RESPONSE_SUCCESS,
+ FUEL_LEVEL_LOW_RESPONSE_SUCCESS, RANGE_REMAINING_RESPONSE_SUCCESS,
+ DISTANCE_DISPLAY_UNITS_RESPONSE_SUCCESS);
- AtomicReference<EnergyLevel> loadedResult = new AtomicReference<>();
- OnCarDataAvailableListener<EnergyLevel> listener = (data) -> {
- loadedResult.set(data);
- mCountDownLatch.countDown();
- };
+ EnergyLevel expectedEnergyLevel = new EnergyLevel.Builder().setBatteryPercent(
+ EXPECTED_BATTERY_PERCENT_SUCCESS).setFuelPercent(
+ EXPECTED_FUEL_PERCENT_SUCCESS).setEnergyIsLow(
+ EXPECTED_ENERGY_IS_LOW_SUCCESS).setRangeRemainingMeters(
+ EXPECTED_RANGE_REMAINING_METERS_SUCCESS).setDistanceDisplayUnit(
+ EXPECTED_DISTANCE_DISPLAY_UNIT_SUCCESS).build();
- mAutomotiveCarInfo.addEnergyLevelListener(mExecutor, listener);
-
- // Create "EV_BATTERY_LEVEL", "FUEL_LEVEL", "FUEL_LEVEL_LOW", "RANGE_REMAINING",
- // "DISTANCE_DISPLAY_UNITS" and "FUEL_VOLUME_DISPLAY_UNITS" property IDs list with car
- // zones.
- Map<Integer, List<CarZone>> propertyIdsWithCarZones =
- ImmutableMap.<Integer, List<CarZone>>builder()
- .put(EV_BATTERY_LEVEL, mCarZones)
- .put(FUEL_LEVEL, mCarZones)
- .put(FUEL_LEVEL_LOW, mCarZones)
- .put(RANGE_REMAINING, mCarZones)
- .put(DISTANCE_DISPLAY_UNITS, mCarZones)
- .put(FUEL_VOLUME_DISPLAY_UNITS, mCarZones)
- .buildKeepingLast();
-
- verify(mPropertyManager, times(1)).submitGetPropertyRequest(eq(mGetPropertyRequests),
- eq(mExecutor));
- verify(mPropertyManager, times(1)).submitRegisterListenerRequest(
- eq(propertyIdsWithCarZones), eq(DEFAULT_SAMPLE_RATE), captor.capture(),
- eq(mExecutor));
-
- // Missing response for FUEL_VOLUME_DISPLAY_UNITS.
- mResponse.add(CarPropertyResponse.builder().setPropertyId(EV_BATTERY_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(evBatteryLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL).setStatus(
- STATUS_SUCCESS).setValue(fuelLevelValue).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(FUEL_LEVEL_LOW).setStatus(
- STATUS_SUCCESS).setValue(true).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(RANGE_REMAINING).setStatus(
- STATUS_SUCCESS).setValue(5f).setTimestampMillis(1L).build());
- mResponse.add(CarPropertyResponse.builder().setPropertyId(DISTANCE_DISPLAY_UNITS).setStatus(
- STATUS_SUCCESS).setValue(meterDistanceUnit).setTimestampMillis(1L).build());
-
- captor.getValue().onCarPropertyResponses(mResponse);
- mCountDownLatch.await();
-
- // The partial responses will be returned successfully.
- EnergyLevel energyLevel = loadedResult.get();
- assertThat(energyLevel.getBatteryPercent().getValue()).isEqualTo(
- evBatteryLevelValue / evBatteryCapacity * 100);
- assertThat(energyLevel.getFuelPercent().getValue()).isEqualTo(
- fuelLevelValue / fuelCapacity * 100);
- assertThat(energyLevel.getEnergyIsLow().getValue()).isEqualTo(true);
- assertThat(energyLevel.getRangeRemainingMeters().getValue()).isEqualTo(5f);
- assertThat(energyLevel.getDistanceDisplayUnit().getValue()).isEqualTo(2);
+ getEnergyLevelHelperFunction(energyCapacities, energyResponses, expectedEnergyLevel);
}
}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
index 8bd55d5..01cd107 100644
--- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/UserInteractionsDemoScreen.java
@@ -34,6 +34,7 @@
import androidx.car.app.sample.showcase.common.R;
import androidx.car.app.sample.showcase.common.audio.VoiceInteraction;
import androidx.car.app.sample.showcase.common.screens.userinteractions.RequestPermissionMenuDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.userinteractions.TaskOverflowDemoScreen;
import androidx.core.graphics.drawable.IconCompat;
/** A screen demonstrating User Interactions */
@@ -104,21 +105,40 @@
* Returns the row for TaskRestriction Demo
*/
private Item buildRowForTaskRestrictionDemo() {
+ boolean isInOverflow = mStep >= MAX_STEPS_ALLOWED;
+ String title = isInOverflow ? getCarContext().getString(
+ R.string.application_overflow_title) :
+ getCarContext().getString(R.string.task_step_of_title, mStep,
+ MAX_STEPS_ALLOWED);
+ String subTitle = isInOverflow
+ ?
+ getCarContext().getString(R.string.task_step_of_title, mStep,
+ MAX_STEPS_ALLOWED) :
+ getCarContext().getString(R.string.task_step_of_text);
return new Row.Builder()
- .setTitle(getCarContext().getString(R.string.task_step_of_title, mStep,
- MAX_STEPS_ALLOWED))
- .addText(getCarContext().getString(R.string.task_step_of_text))
+ .setTitle(title)
+ .addText(subTitle)
.setImage(new CarIcon.Builder(
IconCompat.createWithResource(
getCarContext(), R.drawable.baseline_task_24))
.build(), Row.IMAGE_TYPE_ICON)
.setOnClickListener(
- () ->
+ () -> {
+ if (mStep < MAX_STEPS_ALLOWED) {
getScreenManager()
.pushForResult(
new UserInteractionsDemoScreen(
mStep + 1, getCarContext()),
- result -> mIsBackOperation = true))
+ result -> mIsBackOperation = true);
+ } else {
+ getScreenManager()
+ .pushForResult(
+ new TaskOverflowDemoScreen(
+ getCarContext()),
+ result -> mIsBackOperation = true);
+ }
+ }
+ )
.build();
}
diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java
new file mode 100644
index 0000000..7c44b5e
--- /dev/null
+++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/TaskOverflowDemoScreen.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.sample.showcase.common.screens.userinteractions;
+
+import static androidx.car.app.model.Action.BACK;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.CarContext;
+import androidx.car.app.Screen;
+import androidx.car.app.constraints.ConstraintManager;
+import androidx.car.app.model.ItemList;
+import androidx.car.app.model.ListTemplate;
+import androidx.car.app.model.Row;
+import androidx.car.app.model.Template;
+import androidx.car.app.sample.showcase.common.R;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.GridTemplateMenuDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.ListTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.MessageTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.PaneTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.SearchTemplateDemoScreen;
+import androidx.car.app.sample.showcase.common.screens.templatelayouts.SignInTemplateDemoScreen;
+
+/** A screen demonstrating Task Overflow for the different templates */
+public class TaskOverflowDemoScreen extends Screen {
+
+ public TaskOverflowDemoScreen(@NonNull CarContext carContext) {
+ super(carContext);
+ }
+
+ private Row createRow(int index) {
+ switch (index) {
+ case 0:
+ return new Row.Builder()
+ .setTitle(" ")
+ .addText(getCarContext().getString(R.string.application_overflow_subtitle1))
+ .addText(getCarContext().getString(R.string.application_overflow_subtitle2))
+
+ .build();
+ case 1:
+ return buildRowForTemplate(new ListTemplateDemoScreen(getCarContext()),
+ R.string.list_template_demo_title);
+ case 2:
+ return buildRowForTemplate(new GridTemplateMenuDemoScreen(getCarContext()),
+ R.string.grid_template_menu_demo_title);
+ case 3:
+ return buildRowForTemplate(new MessageTemplateDemoScreen(getCarContext()),
+ R.string.msg_template_demo_title);
+ case 4:
+ return buildRowForTemplate(new PaneTemplateDemoScreen(getCarContext()),
+ R.string.pane_template_demo_title);
+ case 5:
+ return buildRowForTemplate(new SearchTemplateDemoScreen(getCarContext()),
+ R.string.search_template_demo_title);
+ case 6:
+ return buildRowForTemplate(new SignInTemplateDemoScreen(getCarContext()),
+ R.string.sign_in_template_demo_title);
+ default:
+ return new Row.Builder()
+ .setTitle(
+ getCarContext().getString(R.string.other_row_title_prefix) + (index
+ + 1))
+ .addText(getCarContext().getString(R.string.other_row_text))
+ .build();
+ }
+ }
+
+ private Row buildRowForTemplate(Screen screen, int title) {
+ return new Row.Builder()
+ .setTitle(getCarContext().getString(title))
+ .setOnClickListener(() -> getScreenManager().push(screen))
+ .setBrowsable(true)
+ .build();
+ }
+
+ @NonNull
+ @Override
+ public Template onGetTemplate() {
+ int listLimit = getCarContext().getCarService(ConstraintManager.class).getContentLimit(
+ ConstraintManager.CONTENT_LIMIT_TYPE_LIST);
+ if (listLimit >= 6) {
+ listLimit = 6;
+ }
+
+ ItemList.Builder listBuilder = new ItemList.Builder();
+ for (int i = 0; i < listLimit; i++) {
+ listBuilder.addItem(createRow(i));
+ }
+ return new ListTemplate.Builder()
+ .setSingleList(listBuilder.build())
+ .setTitle(getCarContext().getString(R.string.application_overflow_title))
+ .setHeaderAction(BACK)
+ .build();
+ }
+}
diff --git a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
index 197eb29..9bbe73a 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values/strings.xml
@@ -373,6 +373,7 @@
<string name="task_limit_reached_msg">Task limit reached\nGoing forward will force stop the app</string>
<string name="task_step_of_title">Task step %1$d of %2$d</string>
<string name="task_step_of_text">Click to go forward</string>
+ <string name="task_content_allowed">Please visit the different templates and ensure the car is in driving mode</string>
<!-- ToggleButtonDemoScreen -->
<string name="toggle_button_demo_title">Toggle Button Demo</string>
@@ -411,6 +412,11 @@
<string name="user_interactions_demo_title">User Interactions</string>
<string name="request_permission_menu_demo_title">Request Permissions Demos</string>
+ <!-- Task Overflow Demo Screen -->
+ <string name="application_overflow_title">Application Overflow Validator</string>
+ <string name="application_overflow_subtitle1">Please test the following templates while changing</string>
+ <string name="application_overflow_subtitle2">the vehicle from parked to driving state</string>
+
<!-- Manifest file permissions -->
<string name="perm_group">Permission Group</string>
<string name="perm_group_description">Permission Group for Showcase App</string>
diff --git a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
index 9908df5..8157779 100644
--- a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
@@ -39,6 +39,7 @@
field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+ field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
@@ -843,6 +844,14 @@
}
+package androidx.car.app.messaging {
+
+ @androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
+ field public static final String ACTION_HANDLE_CAR_MESSAGING = "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+ }
+
+}
+
package androidx.car.app.messaging.model {
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 9908df5..8157779 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -39,6 +39,7 @@
field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+ field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
@@ -843,6 +844,14 @@
}
+package androidx.car.app.messaging {
+
+ @androidx.car.app.annotations.ExperimentalCarApi public class MessagingServiceConstants {
+ field public static final String ACTION_HANDLE_CAR_MESSAGING = "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+ }
+
+}
+
package androidx.car.app.messaging.model {
@androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 4a8c423..3f4b442 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -136,6 +136,14 @@
@ExperimentalCarApi
public static final String CATEGORY_SETTINGS_APP = "androidx.car.app.category.SETTINGS";
+ /**
+ * Used to declare that this app is a messaging app in the manifest.
+ *
+ * <p> This app can be used to send and receive short-form chat messages (IM/SMS).
+ */
+ @ExperimentalCarApi
+ public static final String CATEGORY_MESSAGING_APP = "androidx.car.app.category.MESSAGING";
+
private static final String AUTO_DRIVE = "AUTO_DRIVE";
@NonNull
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java b/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java
new file mode 100644
index 0000000..ef46c87
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/MessagingServiceConstants.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.messaging;
+
+import androidx.car.app.annotations.ExperimentalCarApi;
+
+/** Constants related to messaging in Android Auto */
+@ExperimentalCarApi
+public class MessagingServiceConstants {
+ /**
+ * Used to declare Android Auto messaging support within an app's manifest
+ *
+ * <p> Specifically, this bit should be added to an {@link android.app.IntentService} as the
+ * {@code IntentFilter}'s action. When declared, apps will show up in Android Auto's app
+ * launcher with the "default" / "built-in" in-car messaging experience.
+ */
+ public static final String ACTION_HANDLE_CAR_MESSAGING =
+ "androidx.car.app.messaging.action.HANDLE_CAR_MESSAGING";
+
+ // Do not instantiate
+ private MessagingServiceConstants() {}
+}
diff --git a/collection/collection-benchmark/build.gradle b/collection/collection-benchmark/build.gradle
index 7832f49..1cec99e 100644
--- a/collection/collection-benchmark/build.gradle
+++ b/collection/collection-benchmark/build.gradle
@@ -16,7 +16,6 @@
import androidx.build.KmpPlatformsKt
-import androidx.build.Publish
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.BitcodeEmbeddingMode
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFrameworkConfig
@@ -27,6 +26,7 @@
id("AndroidXPlugin")
id("com.android.library")
id("androidx.benchmark")
+ id("androidx.benchmark.darwin")
}
def macEnabled = KmpPlatformsKt.enableMac(project)
@@ -34,7 +34,7 @@
androidXMultiplatform {
android()
- // XCFrameworkConfig must match module name
+ // XCFrameworkConfig must always be called AndroidXDarwinBenchmarks
def xcf = new XCFrameworkConfig(project, "AndroidXDarwinBenchmarks")
ios {
binaries.framework {
@@ -111,18 +111,14 @@
description = "AndroidX Collections Benchmarks (Android / iOS)"
}
-if (macEnabled) {
- apply plugin: "androidx.benchmark.darwin"
- darwinBenchmark {
- xcodeGenConfigFile = project.rootProject.file(
- "benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml"
- )
- xcodeProjectName = "collection-benchmark-ios"
- scheme = "testapp-ios"
- // ios 13, 15.2
- destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
- xcFrameworkConfig = "AndroidXDarwinBenchmarks"
- }
+darwinBenchmark {
+ xcodeGenConfigFile = project.rootProject.file(
+ "benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml"
+ )
+ xcodeProjectName = "collection-benchmark-ios"
+ scheme = "testapp-ios"
+ // ios 13, 15.2
+ destination = "platform=iOS Simulator,name=iPhone 13,OS=15.2"
}
android {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
index c7872c7..debf3c6 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimateAsState.kt
@@ -21,6 +21,7 @@
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.geometry.Offset
@@ -396,6 +397,7 @@
finishedListener: ((T) -> Unit)? = null
): State<T> {
+ val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
val animatable = remember { Animatable(targetValue, typeConverter, visibilityThreshold, label) }
val listener by rememberUpdatedState(finishedListener)
val animSpec: AnimationSpec<T> by rememberUpdatedState(
@@ -429,7 +431,7 @@
}
}
}
- return animatable.asState()
+ return toolingOverride.value ?: animatable.asState()
}
@Deprecated(
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 c9657c0..c1d0123 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
@@ -28,6 +28,7 @@
) = verifyComposeIrTransform(
"""
import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.key
import androidx.compose.runtime.NonRestartableComposable
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 fc62647..0f96e97 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
@@ -287,7 +287,7 @@
fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<M3>,<A()>:Test.kt")
- val tmp1_marker = %composer.currentMarker
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -303,7 +303,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (condition) {
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -361,7 +361,7 @@
fun Test(a: Boolean, b: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<M3>,<M3>,<A()>:Test.kt")
- val tmp1_marker = %composer.currentMarker
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
@@ -380,7 +380,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (a) {
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -401,7 +401,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (b) {
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -722,7 +722,7 @@
fun testInline_M1_W_Return_Func(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(testInline_M1_W_Return_Func)<A()>,<M1>,<A()>:Test.kt")
- val tmp1_marker = %composer.currentMarker
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -742,7 +742,7 @@
while (true) {
A(%composer, 0)
if (condition) {
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -882,7 +882,7 @@
fun test_CM1_CCM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(test_CM1_CCM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
- val tmp1_marker = %composer.currentMarker
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -906,7 +906,7 @@
sourceInformation(%composer, "C<Text("...>:Test.kt")
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
Text("In CCM1", %composer, 0b0110)
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
@@ -998,6 +998,295 @@
"""
)
+ @Test // regression 255350755
+ fun testEnsureEarlyExitInNonInline_NormalComposable() = controlFlow(
+ """
+ object obj {
+ val condition = true
+ }
+
+ @Composable
+ fun Test(condition: Boolean) {
+ if (condition) return
+ with (obj) {
+ if (condition) return
+ }
+ A()
+ }
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ object obj {
+ val condition: Boolean = true
+ static val %stable: Int = 0
+ }
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<A()>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ if (condition) {
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ with(obj) {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ }
+ A(%composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test // regression 255350755
+ fun testEnsureEarlyExitInNonInline_ReadOnlyComposable() = controlFlow(
+ """
+ import androidx.compose.runtime.currentComposer
+
+ object obj {
+ val condition = false
+ }
+
+ @Composable
+ @ReadOnlyComposable
+ fun Calculate(condition: Boolean): Boolean {
+ if (condition) return false
+
+ with (obj) {
+ if (condition) return false
+ return currentComposer.inserting
+ }
+ }
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ object obj {
+ val condition: Boolean = false
+ static val %stable: Int = 0
+ }
+ @Composable
+ @ReadOnlyComposable
+ fun Calculate(condition: Boolean, %composer: Composer?, %changed: Int): Boolean {
+ sourceInformationMarkerStart(%composer, <>, "C(Calculate):Test.kt")
+ val tmp1_marker = %composer.currentMarker
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ if (condition) {
+ val tmp0_return = false
+ sourceInformationMarkerEnd(%composer)
+ return tmp0_return
+ }
+ with(obj) {
+ if (condition) {
+ val tmp0_return = false
+ %composer.endToMarker(tmp1_marker)
+ sourceInformationMarkerEnd(%composer)
+ return tmp0_return
+ }
+ val tmp1_return = %composer.inserting
+ %composer.endToMarker(tmp1_marker)
+ sourceInformationMarkerEnd(%composer)
+ return tmp1_return
+ }
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ sourceInformationMarkerEnd(%composer)
+ }
+ """
+ )
+
+ @Test // regression 255350755
+ fun testEnsureEarlyExitInInline_Labeled() = controlFlow(
+ """
+ @Composable
+ fun Test(condition: Boolean) {
+ IW iw@ {
+ if (condition) return@iw
+ A()
+ }
+ }
+ """,
+ """
+ @Composable
+ fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(Test)<IW>:Test.kt")
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0_marker = %composer.currentMarker
+ IW({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<A()>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ }
+ A(%composer, 0)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """
+ )
+
+ @Test
+ fun testEnsureRuntimeTestWillCompile_CL() = ensureSetup {
+ classLoader(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun test_CM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ M1 {
+ Text("M1 - before")
+ if (condition) return
+ Text("M1 - after")
+ }
+ Text("Root - after")
+ }
+ @Composable
+ inline fun InlineWrapper(content: @Composable () -> Unit) = content()
+
+ @Composable
+ inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
+
+ @Composable
+ fun Text(value: String) { }
+ """,
+ "Test.kt"
+ )
+ }
+
+ @Test // regression 255350755
+ fun testEnsureRuntimeTestWillCompile_CG() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun test_CM1_RetFun(condition: Boolean) {
+ Text("Root - before")
+ M1 {
+ Text("M1 - before")
+ if (condition) return
+ Text("M1 - after")
+ }
+ Text("Root - after")
+ }
+
+ """,
+ """
+ @Composable
+ fun test_CM1_RetFun(condition: Boolean, %composer: Composer?, %changed: Int) {
+ %composer = %composer.startRestartGroup(<>)
+ sourceInformation(%composer, "C(test_CM1_RetFun)<Text("...>,<M1>,<Text("...>:Test.kt")
+ val tmp0_marker = %composer.currentMarker
+ val %dirty = %changed
+ if (%changed and 0b1110 === 0) {
+ %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
+ }
+ if (%dirty and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ Text("Root - before", %composer, 0b0110)
+ M1({ %composer: Composer?, %changed: Int ->
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C<Text("...>,<Text("...>:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ Text("M1 - before", %composer, 0b0110)
+ if (condition) {
+ %composer.endToMarker(tmp0_marker)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ return
+ }
+ Text("M1 - after", %composer, 0b0110)
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endReplaceableGroup()
+ }, %composer, 0)
+ Text("Root - after", %composer, 0b0110)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+ test_CM1_RetFun(condition, %composer, updateChangedFlags(%changed or 0b0001))
+ }
+ }
+ """,
+ extra = """
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.NonRestartableComposable
+
+ @Composable
+ inline fun InlineWrapper(content: @Composable () -> Unit) = content()
+
+ @Composable
+ inline fun M1(content: @Composable () -> Unit) = InlineWrapper { content() }
+
+ @Composable @NonRestartableComposable
+ fun Text(value: String) { }
+ """
+ )
+
@Test
fun testIfElseWithCallsInConditions(): Unit = controlFlow(
"""
@@ -5299,8 +5588,6 @@
@Test
fun testReadOnlyComposableWithEarlyReturn() = controlFlow(
source = """
- import androidx.compose.runtime.ReadOnlyComposable
-
@ReadOnlyComposable
@Composable
fun getSomeValue(a: Int): Int {
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
index 4159214..136d1d6 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TraceInformationTest.kt
@@ -118,7 +118,7 @@
fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
%composer = %composer.startRestartGroup(<>)
sourceInformation(%composer, "C(Test)<A()>,<Wrappe...>,<A()>:Test.kt")
- val tmp1_marker = %composer.currentMarker
+ val tmp0_marker = %composer.currentMarker
val %dirty = %changed
if (%changed and 0b1110 === 0) {
%dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -134,7 +134,7 @@
if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
A(%composer, 0)
if (!condition) {
- %composer.endToMarker(tmp1_marker)
+ %composer.endToMarker(tmp0_marker)
if (isTraceInProgress()) {
traceEventEnd()
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 980e7f7..b29e3df 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -110,7 +110,7 @@
* The maven version string of this compiler. This string should be updated before/after every
* release.
*/
- const val compilerVersion: String = "1.3.3"
+ const val compilerVersion: String = "1.4.0-alpha01"
private val minimumRuntimeVersion: String
get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
}
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 96d4611..9f41e35 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
@@ -1849,8 +1849,11 @@
)
}
- fun irCurrentMarker() =
- irMethodCall(irCurrentComposer(), currentMarkerProperty!!.getter!!)
+ fun irCurrentMarker(composerParameter: IrValueParameter) =
+ irMethodCall(
+ irCurrentComposer(composerParameter = composerParameter),
+ currentMarkerProperty!!.getter!!
+ )
private fun irIsSkipping() =
irMethodCall(irCurrentComposer(), isSkippingFunction.getter!!)
@@ -1971,18 +1974,17 @@
}
}
- private fun nearestComposer(): IrValueParameter =
- currentScope.nearestComposer
- ?: error("Not in a composable function \n${printScopeStack()}")
+ private fun nearestComposer(): IrValueParameter = currentScope.myComposer
private fun irCurrentComposer(
startOffset: Int = UNDEFINED_OFFSET,
- endOffset: Int = UNDEFINED_OFFSET
+ endOffset: Int = UNDEFINED_OFFSET,
+ composerParameter: IrValueParameter = nearestComposer()
): IrExpression {
return IrGetValueImpl(
startOffset,
endOffset,
- nearestComposer().symbol
+ composerParameter.symbol
)
}
@@ -2620,9 +2622,9 @@
val marker = irGet(functionScope.allocateMarker())
extraEndLocation(irEndToMarker(marker))
} else {
+ val marker = functionScope.allocateMarker()
functionScope.markReturn {
- val marker = irGet(functionScope.allocateMarker())
- extraEndLocation(irEndToMarker(marker))
+ extraEndLocation(irEndToMarker(irGet(marker)))
extraEndLocation(it)
}
}
@@ -3713,6 +3715,9 @@
open val fileScope: FileScope? get() = parent?.fileScope
open val nearestComposer: IrValueParameter? get() = parent?.nearestComposer
+ val myComposer: IrValueParameter get() = nearestComposer
+ ?: error("Not in a composable function")
+
open class SourceLocation(val element: IrElement) {
open val repeatable: Boolean
get() = false
@@ -3777,7 +3782,7 @@
return (if (isInlinedLambda && parent is Scope.CallScope) {
parent.allocateMarker()
} else transformer.irTemporary(
- transformer.irCurrentMarker(),
+ transformer.irCurrentMarker(myComposer),
getNameForTemporary("marker")
).also { markerPreamble.statements.add(it) }).also {
marker = it
@@ -4230,7 +4235,7 @@
fun allocateMarker(): IrVariable = marker
?: transformer.irTemporary(
- transformer.irCurrentMarker(),
+ transformer.irCurrentMarker(myComposer),
getNameForTemporary("marker")
).also { marker = it }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
index de3293f..62ab2de 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
@@ -138,13 +138,18 @@
// do it ourself here.
if (
ownerFn != null &&
- ownerFn.origin == IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB
+ ownerFn.origin == IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB &&
+ ownerFn.hasComposableArguments()
) {
- symbolRemapper.visitConstructor(ownerFn)
- val newFn = super.visitConstructor(ownerFn).also {
- it.patchDeclarationParents(ownerFn.parent)
+ if (symbolRemapper.getReferencedConstructor(ownerFn.symbol) == ownerFn.symbol) {
+ // Not remapped yet, so remap now.
+ // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+ symbolRemapper.visitConstructor(ownerFn)
+ super.visitConstructor(ownerFn).also {
+ it.patchDeclarationParents(ownerFn.parent)
+ }
}
- val newCallee = symbolRemapper.getReferencedConstructor(newFn.symbol)
+ val newCallee = symbolRemapper.getReferencedConstructor(ownerFn.symbol)
return IrConstructorCallImpl(
expression.startOffset, expression.endOffset,
@@ -192,22 +197,29 @@
expression.dispatchReceiver?.type?.isComposable() == true
) {
val typeArguments = containingClass.defaultType.arguments
- val newFnClass = context.function(typeArguments.size).owner
+ val realParams = typeArguments.size - 1
+ // with composer and changed
+ val newArgsSize = realParams + 1 + changedParamCount(realParams, 0)
+ val newFnClass = context.function(newArgsSize).owner
var newFn = newFnClass
.functions
.first { it.name == ownerFn.name }
- symbolRemapper.visitSimpleFunction(newFn)
- newFn = super.visitSimpleFunction(newFn).also { fn ->
- fn.overriddenSymbols = ownerFn.overriddenSymbols.map { it }
- fn.dispatchReceiverParameter = ownerFn.dispatchReceiverParameter
- fn.extensionReceiverParameter = ownerFn.extensionReceiverParameter
- newFn.valueParameters.forEach { p ->
- fn.addValueParameter(p.name.identifier, p.type)
+ if (symbolRemapper.getReferencedSimpleFunction(newFn.symbol) == newFn.symbol) {
+ // Not remapped yet, so remap now.
+ // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+ symbolRemapper.visitSimpleFunction(newFn)
+ newFn = super.visitSimpleFunction(newFn).also { fn ->
+ fn.overriddenSymbols = ownerFn.overriddenSymbols.map { it }
+ fn.dispatchReceiverParameter = ownerFn.dispatchReceiverParameter
+ fn.extensionReceiverParameter = ownerFn.extensionReceiverParameter
+ newFn.valueParameters.forEach { p ->
+ fn.addValueParameter(p.name.identifier, p.type)
+ }
+ fn.patchDeclarationParents(newFnClass)
+ assert(fn.body == null) { "expected body to be null" }
}
- fn.patchDeclarationParents(newFnClass)
- assert(fn.body == null) { "expected body to be null" }
}
val newCallee = symbolRemapper.getReferencedSimpleFunction(newFn.symbol)
@@ -234,20 +246,30 @@
val property = ownerFn.correspondingPropertySymbol!!.owner
// avoid java properties since they go through a different lowering and it is
// also impossible for them to have composable types
- if (property.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB) {
- symbolRemapper.visitProperty(property)
- visitProperty(property).also {
- it.getter?.correspondingPropertySymbol = it.symbol
- it.setter?.correspondingPropertySymbol = it.symbol
- it.patchDeclarationParents(ownerFn.parent)
- it.copyAttributes(property)
+ if (property.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB &&
+ property.getter?.returnType?.isComposable() == true
+ ) {
+ if (symbolRemapper.getReferencedProperty(property.symbol) == property.symbol) {
+ // Not remapped yet, so remap now.
+ // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+ symbolRemapper.visitProperty(property)
+ visitProperty(property).also {
+ it.getter?.correspondingPropertySymbol = it.symbol
+ it.setter?.correspondingPropertySymbol = it.symbol
+ it.patchDeclarationParents(ownerFn.parent)
+ it.copyAttributes(property)
+ }
}
}
- } else {
- symbolRemapper.visitSimpleFunction(ownerFn)
- visitSimpleFunction(ownerFn).also {
- it.correspondingPropertySymbol = null
- it.patchDeclarationParents(ownerFn.parent)
+ } else if (ownerFn.hasComposableArguments()) {
+ if (symbolRemapper.getReferencedSimpleFunction(ownerFn.symbol) == ownerFn.symbol) {
+ // Not remapped yet, so remap now.
+ // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
+ symbolRemapper.visitSimpleFunction(ownerFn)
+ visitSimpleFunction(ownerFn).also {
+ it.correspondingPropertySymbol = null
+ it.patchDeclarationParents(ownerFn.parent)
+ }
}
}
val newCallee = symbolRemapper.getReferencedSimpleFunction(ownerFn.symbol)
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
index 523769d..c152873 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerParamTransformer.kt
@@ -74,7 +74,6 @@
import org.jetbrains.kotlin.ir.util.copyTypeParametersFrom
import org.jetbrains.kotlin.ir.util.defaultType
import org.jetbrains.kotlin.ir.util.explicitParameters
-import org.jetbrains.kotlin.ir.util.fileOrNull
import org.jetbrains.kotlin.ir.util.findAnnotation
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.getInlineClassUnderlyingType
@@ -167,15 +166,6 @@
else -> (symbol.owner).withComposerParamIfNeeded()
}
- // externally transformed functions are already remapped from decoys, so we only need to
- // add the parameters to the call
- if (!ownerFn.externallyTransformed()) {
- if (!isComposableLambda && !transformedFunctionSet.contains(ownerFn))
- return this
- if (symbol.owner == ownerFn)
- return this
- }
-
return IrCallImpl(
startOffset,
endOffset,
@@ -673,5 +663,7 @@
* different module fragment with the same [ModuleDescriptor]
*/
private fun IrFunction.externallyTransformed(): Boolean =
- decoysEnabled && currentModule?.files?.contains(fileOrNull) != true
+ decoysEnabled && valueParameters.firstOrNull {
+ it.name == KtxNameConventions.COMPOSER_PARAMETER
+ } != null
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
index 1f74d01..2b3ec86 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/WrapJsComposableLambdaLowering.kt
@@ -20,6 +20,8 @@
import androidx.compose.compiler.plugins.kotlin.ComposeClassIds
import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
import androidx.compose.compiler.plugins.kotlin.lower.decoys.AbstractDecoysLowering
+import androidx.compose.compiler.plugins.kotlin.lower.decoys.CreateDecoysTransformer
+import androidx.compose.compiler.plugins.kotlin.lower.decoys.isDecoy
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
import org.jetbrains.kotlin.descriptors.DescriptorVisibilities
@@ -28,6 +30,7 @@
import org.jetbrains.kotlin.ir.UNDEFINED_OFFSET
import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.impl.IrFunctionImpl
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrExpression
@@ -43,6 +46,7 @@
import org.jetbrains.kotlin.ir.types.typeWith
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
import org.jetbrains.kotlin.ir.util.SYNTHETIC_OFFSET
+import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.isVararg
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
@@ -83,12 +87,31 @@
signatureBuilder: IdSignatureSerializer
) : AbstractDecoysLowering(context, symbolRemapper, metrics, signatureBuilder) {
- private val composableLambdaSymbol = symbolRemapper.getReferencedSimpleFunction(
- getTopLevelFunction(ComposeCallableIds.composableLambda)
- )
- private val composableLambdaInstanceSymbol = symbolRemapper.getReferencedSimpleFunction(
- getTopLevelFunction(ComposeCallableIds.composableLambdaInstance)
- )
+ private val rememberFunSymbol by lazy {
+ symbolRemapper.getReferencedSimpleFunction(
+ getTopLevelFunctions(ComposeCallableIds.remember).map { it.owner }.first {
+ it.valueParameters.size == 2 && !it.valueParameters.first().isVararg
+ }.symbol
+ ).owner.let {
+ if (!it.isDecoy()) {
+ // If a module didn't have any explicit remember calls,
+ // so `fun remember` wasn't transformed yet, then we have to transform it now.
+ val createDecoysTransformer = CreateDecoysTransformer(
+ context, symbolRemapper, signatureBuilder, metrics
+ )
+ createDecoysTransformer.visitSimpleFunction(it) as IrSimpleFunction
+ createDecoysTransformer.updateParents()
+ val composerParamTransformer = ComposerParamTransformer(
+ context, symbolRemapper, true, metrics
+ )
+ composerParamTransformer.visitSimpleFunction(
+ it.getComposableForDecoy().owner as IrSimpleFunction
+ ) as IrSimpleFunction
+ } else {
+ it.getComposableForDecoy().owner as IrSimpleFunction
+ }
+ }.symbol
+ }
override fun lower(module: IrModuleFragment) {
module.transformChildrenVoid(this)
@@ -97,11 +120,11 @@
override fun visitCall(expression: IrCall): IrExpression {
val original = super.visitCall(expression) as IrCall
- return when (expression.symbol) {
- composableLambdaSymbol -> {
+ return when (expression.symbol.owner.fqNameForIrSerialization) {
+ ComposeCallableIds.composableLambda.asSingleFqName() -> {
transformComposableLambdaCall(original)
}
- composableLambdaInstanceSymbol -> {
+ ComposeCallableIds.composableLambdaInstance.asSingleFqName() -> {
transformComposableLambdaInstanceCall(original)
}
else -> original
@@ -145,12 +168,6 @@
lambda, irGet(composableLambdaVar)
)
- val rememberFunSymbol = symbolRemapper.getReferencedSimpleFunction(
- getTopLevelFunctions(ComposeCallableIds.remember).first {
- it.owner.valueParameters.size == 2 && !it.owner.valueParameters.first().isVararg
- }
- ).owner.getComposableForDecoy() as IrSimpleFunctionSymbol
-
val calculationFunSymbol = IrSimpleFunctionSymbolImpl()
val rememberBlock = createLambda0(
returnType = lambda.type,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
index 97fca97..bab96bb 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/AbstractDecoysLowering.kt
@@ -16,13 +16,24 @@
package androidx.compose.compiler.plugins.kotlin.lower.decoys
+import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
import androidx.compose.compiler.plugins.kotlin.lower.AbstractComposeLowering
import androidx.compose.compiler.plugins.kotlin.lower.includeFileNameInExceptionTrace
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
+import org.jetbrains.kotlin.ir.declarations.IrConstructor
+import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrFile
+import org.jetbrains.kotlin.ir.declarations.IrFunction
+import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
+import org.jetbrains.kotlin.ir.types.IrSimpleType
+import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.hasAnnotation
+import org.jetbrains.kotlin.ir.util.isEnumClass
+import org.jetbrains.kotlin.ir.util.isLocal
+import org.jetbrains.kotlin.ir.util.parentAsClass
abstract class AbstractDecoysLowering(
pluginContext: IrPluginContext,
@@ -47,4 +58,36 @@
return file
}
}
+
+ protected fun IrFunction.shouldBeRemapped(): Boolean =
+ !isLocalFunction() &&
+ !isEnumConstructor() &&
+ (hasComposableAnnotation() || hasComposableParameter())
+
+ private fun IrFunction.isLocalFunction(): Boolean =
+ origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA ||
+ (isLocal && (this is IrSimpleFunction && !overridesComposable()))
+
+ private fun IrSimpleFunction.overridesComposable() =
+ overriddenSymbols.any {
+ it.owner.isDecoy() || it.owner.shouldBeRemapped()
+ }
+
+ private fun IrFunction.hasComposableParameter() =
+ valueParameters.any { it.type.hasComposable() } ||
+ extensionReceiverParameter?.type?.hasComposable() == true
+
+ private fun IrFunction.isEnumConstructor() =
+ this is IrConstructor && parentAsClass.isEnumClass
+
+ private fun IrType.hasComposable(): Boolean {
+ if (hasAnnotation(ComposeFqNames.Composable)) {
+ return true
+ }
+
+ return when (this) {
+ is IrSimpleType -> arguments.any { (it as? IrType)?.hasComposable() == true }
+ else -> false
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
index e9df6af..ad16985 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/CreateDecoysTransformer.kt
@@ -16,7 +16,6 @@
package androidx.compose.compiler.plugins.kotlin.lower.decoys
-import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
import androidx.compose.compiler.plugins.kotlin.lower.ModuleLoweringPass
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
@@ -32,7 +31,6 @@
import org.jetbrains.kotlin.ir.builders.irReturn
import org.jetbrains.kotlin.ir.declarations.IrConstructor
import org.jetbrains.kotlin.ir.declarations.IrDeclarationContainer
-import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
import org.jetbrains.kotlin.ir.declarations.IrDeclarationParent
import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@@ -42,19 +40,13 @@
import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
import org.jetbrains.kotlin.ir.expressions.IrGetValue
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
-import org.jetbrains.kotlin.ir.types.IrSimpleType
-import org.jetbrains.kotlin.ir.types.IrType
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
import org.jetbrains.kotlin.ir.util.addChild
import org.jetbrains.kotlin.ir.util.constructors
import org.jetbrains.kotlin.ir.util.copyTo
import org.jetbrains.kotlin.ir.util.copyTypeParametersFrom
import org.jetbrains.kotlin.ir.util.defaultType
-import org.jetbrains.kotlin.ir.util.hasAnnotation
import org.jetbrains.kotlin.ir.util.hasDefaultValue
-import org.jetbrains.kotlin.ir.util.isEnumClass
-import org.jetbrains.kotlin.ir.util.isLocal
-import org.jetbrains.kotlin.ir.util.parentAsClass
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.ir.util.remapTypeParameters
import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
@@ -116,12 +108,15 @@
override fun lower(module: IrModuleFragment) {
module.transformChildrenVoid()
+ updateParents()
+ module.patchDeclarationParents()
+ }
+ fun updateParents() {
originalFunctions.forEach { (f, parent) ->
(parent as? IrDeclarationContainer)?.addChild(f)
}
-
- module.patchDeclarationParents()
+ originalFunctions.clear()
}
override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
@@ -189,7 +184,7 @@
newFunction.overriddenSymbols = (original as IrSimpleFunction).overriddenSymbols
newFunction.correspondingPropertySymbol = null
}
- newFunction.origin = IrDeclarationOrigin.DEFINED
+ newFunction.origin = original.origin
// here generic value parameters will be applied
newFunction.copyTypeParametersFrom(original)
@@ -302,39 +297,7 @@
}
}
- private fun IrFunction.shouldBeRemapped(): Boolean =
- !isLocalFunction() &&
- !isEnumConstructor() &&
- (hasComposableAnnotation() || hasComposableParameter())
-
- private fun IrFunction.isLocalFunction(): Boolean =
- origin == IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA ||
- (isLocal && (this is IrSimpleFunction && !overridesComposable()))
-
- private fun IrSimpleFunction.overridesComposable() =
- overriddenSymbols.any {
- it.owner.isDecoy() || it.owner.shouldBeRemapped()
- }
-
- private fun IrFunction.hasComposableParameter() =
- valueParameters.any { it.type.hasComposable() } ||
- extensionReceiverParameter?.type?.hasComposable() == true
-
- private fun IrFunction.isEnumConstructor() =
- this is IrConstructor && parentAsClass.isEnumClass
-
- private fun IrType.hasComposable(): Boolean {
- if (hasAnnotation(ComposeFqNames.Composable)) {
- return true
- }
-
- return when (this) {
- is IrSimpleType -> arguments.any { (it as? IrType)?.hasComposable() == true }
- else -> false
- }
- }
-
companion object {
private const val IMPLEMENTATION_FUNCTION_SUFFIX = "\$composable"
}
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
index ba05ddc..fc763b8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/decoys/SubstituteDecoyCallsTransformer.kt
@@ -17,14 +17,17 @@
package androidx.compose.compiler.plugins.kotlin.lower.decoys
import androidx.compose.compiler.plugins.kotlin.ModuleMetrics
+import androidx.compose.compiler.plugins.kotlin.lower.ComposerParamTransformer
import androidx.compose.compiler.plugins.kotlin.lower.ModuleLoweringPass
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
import org.jetbrains.kotlin.ir.IrStatement
import org.jetbrains.kotlin.ir.declarations.IrConstructor
+import org.jetbrains.kotlin.ir.declarations.IrFunction
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.declarations.lazy.IrLazyFunctionBase
import org.jetbrains.kotlin.ir.expressions.IrCall
import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
import org.jetbrains.kotlin.ir.expressions.IrDelegatingConstructorCall
@@ -35,11 +38,14 @@
import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrDelegatingConstructorCallImpl
import org.jetbrains.kotlin.ir.expressions.impl.IrFunctionReferenceImpl
+import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
import org.jetbrains.kotlin.ir.util.copyTypeAndValueArgumentsFrom
import org.jetbrains.kotlin.ir.util.patchDeclarationParents
import org.jetbrains.kotlin.ir.util.remapTypeParameters
+import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
/**
* Replaces all decoys references to their implementations created in [CreateDecoysTransformer].
@@ -55,6 +61,10 @@
metrics = metrics,
signatureBuilder = signatureBuilder
), ModuleLoweringPass {
+ private val decoysTransformer = CreateDecoysTransformer(
+ pluginContext, symbolRemapper, signatureBuilder, metrics
+ )
+ private val lazyDeclarationsCache = mutableMapOf<IrFunctionSymbol, IrFunction>()
override fun lower(module: IrModuleFragment) {
module.transformChildrenVoid()
@@ -117,7 +127,7 @@
}
override fun visitConstructorCall(expression: IrConstructorCall): IrExpression {
- val callee = expression.symbol.owner
+ val callee = expression.symbol.decoyOwner
if (!callee.isDecoy()) {
return super.visitConstructorCall(expression)
}
@@ -144,7 +154,7 @@
override fun visitDelegatingConstructorCall(
expression: IrDelegatingConstructorCall
): IrExpression {
- val callee = expression.symbol.owner
+ val callee = expression.symbol.decoyOwner
if (!callee.isDecoy()) {
return super.visitDelegatingConstructorCall(expression)
}
@@ -167,7 +177,7 @@
}
override fun visitCall(expression: IrCall): IrExpression {
- val callee = expression.symbol.owner
+ val callee = expression.symbol.decoyOwner
if (!callee.isDecoy()) {
return super.visitCall(expression)
}
@@ -191,7 +201,7 @@
}
override fun visitFunctionReference(expression: IrFunctionReference): IrExpression {
- val callee = expression.symbol.owner
+ val callee = expression.symbol.decoyOwner
if (!callee.isDecoy()) {
return super.visitFunctionReference(expression)
}
@@ -213,4 +223,43 @@
}
return super.visitFunctionReference(updatedReference)
}
+
+ private val addComposerParameterInplace = object : IrElementTransformerVoid() {
+ private val сomposerParamTransformer = ComposerParamTransformer(
+ context, symbolRemapper, true, metrics
+ )
+ override fun visitSimpleFunction(declaration: IrSimpleFunction): IrStatement {
+ return сomposerParamTransformer.visitSimpleFunction(declaration)
+ }
+ }
+
+ /**
+ * Since kotlin 1.7.20, k/js started to use LazyIr (stubs).
+ * Decoys logic used to rely on full deserialized IR of the module dependencies.
+ *
+ * This extension adjusts LazyIr for decoys needs:
+ * generates decoy implementation declarations and adds composer parameter to ensure matching
+ * signatures.
+ *
+ * To consider: LazyIr allows to get rid of decoys logic. But it's going to be a breaking
+ * change for the compose users, since all published klibs will need to be recompiled in order
+ * to be compatible with new-no-decoys compiler plugin. So we'll need to choose
+ * a good moment for such a breaking change.
+ */
+ private val IrFunctionSymbol.decoyOwner: IrFunction
+ get() = if (owner is IrLazyFunctionBase && !owner.isDecoy()) {
+ lazyDeclarationsCache.getOrPut(this) {
+ val declaration = owner
+ if (declaration.shouldBeRemapped()) {
+ when (declaration) {
+ is IrSimpleFunction -> decoysTransformer.visitSimpleFunction(declaration)
+ is IrConstructor -> decoysTransformer.visitConstructor(declaration)
+ else -> decoysTransformer.visitFunction(declaration)
+ }.also {
+ decoysTransformer.updateParents()
+ owner.parent.transformChildrenVoid(addComposerParameterInplace)
+ } as IrFunction
+ } else owner
+ }
+ } else owner
}
\ No newline at end of file
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
index 894b35c..28cef82 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/WindowInsetsAnimationTest.kt
@@ -25,6 +25,7 @@
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import java.util.concurrent.TimeUnit
import org.junit.After
@@ -89,6 +90,7 @@
}
@OptIn(ExperimentalLayoutApi::class)
+ @FlakyTest(bugId = 256020254)
@Test
fun imeAnimationWhenHidingIme() {
val imeAnimationSourceValues = mutableListOf<Int>()
@@ -133,4 +135,4 @@
imeAnimationTargetValues.last() == imeAnimationSourceValues.first()
}
}
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
similarity index 98%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
index 3a4e5d0..442fe98 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldTrickyUseCase.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/RejectTextChangeDemo.kt
@@ -29,7 +29,7 @@
@Preview
@Composable
-fun InputFieldTrickyUseCase() {
+fun RejectTextChangeDemo() {
LazyColumn {
item {
TagLine(tag = "don't set if non number is added")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 54610b2..2b5a89f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -22,8 +22,7 @@
val TextDemos = DemoCategory(
"Text",
listOf(
- ComposableDemo("Hyphens") { TextDemoHyphens() },
- ComposableDemo("Static text") { TextDemo() },
+ ComposableDemo("Text Accessibility") { TextAccessibilityDemo() },
DemoCategory(
"Text Canvas",
listOf(
@@ -32,57 +31,89 @@
ComposableDemo("Stroke") { TextStrokeDemo() }
)
),
- ComposableDemo("Ellipsize") { EllipsizeDemo() },
- ComposableDemo("Typeface") { TypefaceDemo() },
- ComposableDemo("Variable Fonts") { VariableFontsDemo() },
- ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
- ComposableDemo("All system font families") { SystemFontFamilyDemo() },
- ComposableDemo("Text selection") { TextSelectionDemo() },
- ComposableDemo("Text selection sample") { TextSelectionSample() },
- ComposableDemo("Multi paragraph") { MultiParagraphDemo() },
- ComposableDemo("IncludeFontPadding & Clip") { TextFontPaddingDemo() },
- ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
- ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
- ComposableDemo("Interactive text") { InteractiveTextDemo() },
- ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
- ComposableDemo("Ellipsize and letterspacing") { EllipsizeWithLetterSpacing() },
- ComposableDemo("Line breaking") { TextLineBreakingDemo() },
DemoCategory(
- "Text Overflow",
+ "Text Layout",
listOf(
- ComposableDemo("TextOverflow demo") { TextOverflowDemo() },
- ComposableDemo("Visible overflow in drawText") {
- TextOverflowVisibleInDrawText()
- },
- ComposableDemo("Visible overflow in Popup") { TextOverflowVisibleInPopupDemo() }
+ ComposableDemo("Static text") { TextDemo() },
+ DemoCategory(
+ "Line breaking",
+ listOf(
+ ComposableDemo("Line breaking") { TextLineBreakingDemo() },
+ ComposableDemo("Hyphens") { TextDemoHyphens() },
+ ComposableDemo("Ellipsize") { EllipsizeDemo() },
+ ComposableDemo("Ellipsize and letterspacing") {
+ EllipsizeWithLetterSpacing()
+ },
+ )
+ ),
+ DemoCategory(
+ "Text Overflow",
+ listOf(
+ ComposableDemo("TextOverflow demo") { TextOverflowDemo() },
+ ComposableDemo("Visible overflow in drawText") {
+ TextOverflowVisibleInDrawText()
+ },
+ ComposableDemo("Visible overflow in Popup") {
+ TextOverflowVisibleInPopupDemo()
+ },
+ ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
+ )
+ ),
+ ComposableDemo("IncludeFontPadding & Clip") { TextFontPaddingDemo() },
+ ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
+ ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
+ ComposableDemo("Multi paragraph") { MultiParagraphDemo() },
+ ComposableDemo("Interactive text") { InteractiveTextDemo() },
)
),
DemoCategory(
- "Input fields",
+ "Fonts",
+ listOf(
+ ComposableDemo("Typeface") { TypefaceDemo() },
+ ComposableDemo("Variable Fonts") { VariableFontsDemo() },
+ ComposableDemo("FontFamily fallback") { FontFamilyDemo() },
+ ComposableDemo("All system font families") { SystemFontFamilyDemo() },
+ )
+ ),
+ DemoCategory(
+ "Text Input",
listOf(
ComposableDemo("Basic input fields") { InputFieldDemo() },
- ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
- ComposableDemo("Ime Action") { ImeActionDemo() },
- ComposableDemo("Various input fields") { VariousInputFieldDemo() },
- ComposableDemo("Tricky input field") { InputFieldTrickyUseCase() },
- ComposableDemo("Focus transition") { TextFieldFocusTransition() },
- ComposableDemo("Focus keyboard interaction") {
- TextFieldFocusKeyboardInteraction()
+ ComposableDemo("Capitalization/AutoCorrect") {
+ CapitalizationAutoCorrectDemo()
},
- ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
- ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
- ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
+ ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() },
+ DemoCategory(
+ "Focus",
+ listOf(
+ ComposableDemo("Focus transition") { TextFieldFocusTransition() },
+ ComposableDemo("Focus keyboard interaction") {
+ TextFieldFocusKeyboardInteraction()
+ },
+ )
+ ),
+ ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
+ ComposableDemo("Ime Action") { ImeActionDemo() },
ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
- ComposableDemo("Capitalization/AutoCorrect") { CapitalizationAutoCorrectDemo() },
- ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
ComposableDemo("Inside Dialog") { onNavigateUp ->
DialogInputFieldDemo(onNavigateUp)
},
ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() },
- ComposableDemo("Cursor configuration") { TextFieldCursorBlinkingDemo() },
- ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
+ ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
+ ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
+ ComposableDemo("Reject Text Change") { RejectTextChangeDemo() },
+ ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
+ ComposableDemo("Visual Transformation") { VisualTransformationDemo() },
+ ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
+ ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
)
),
- ComposableDemo("Text Accessibility") { TextAccessibilityDemo() }
+ DemoCategory(
+ "Selection",
+ listOf(
+ ComposableDemo("Text selection") { TextSelectionDemo() },
+ ComposableDemo("Text selection sample") { TextSelectionSample() },
+ )
+ )
)
)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
index dd5c30b..880f509 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldCursorBlinkingDemo.kt
@@ -86,7 +86,7 @@
text = "Normal blink",
selection = TextRange(3)
)
- BasicTextField(value = textFieldValue, >
+ BasicTextField(value = textFieldValue, modifier = demoTextFieldModifiers, >
}
@Composable
@@ -97,6 +97,7 @@
)
BasicTextField(
value = textFieldValue,
+ modifier = demoTextFieldModifiers,
>
cursorBrush = SolidColor(Color.Red)
)
@@ -130,7 +131,7 @@
value = textFieldValue,
>
cursorBrush = SolidColor(color.value),
- modifier = Modifier.onFocusChanged { shouldAnimate = it.isFocused }
+ modifier = demoTextFieldModifiers.onFocusChanged { shouldAnimate = it.isFocused }
)
}
@@ -143,6 +144,7 @@
BasicTextField(
value = textFieldValue,
+ modifier = demoTextFieldModifiers,
>
cursorBrush = Brush.verticalGradient(colors = Rainbow),
)
@@ -168,7 +170,7 @@
BasicTextField(
value = textFieldValue,
>
- modifier = Modifier.onFocusChanged { animate = it.isFocused }
+ modifier = demoTextFieldModifiers.onFocusChanged { animate = it.isFocused }
)
}
@@ -190,6 +192,7 @@
Column {
BasicTextField(
value = textFieldValue,
+ modifier = demoTextFieldModifiers,
>
textStyle = TextStyle.Default.copy(fontFamily = FontFamily.Monospace)
)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
similarity index 99%
rename from compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
rename to compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
index d51f8f2..7889ff5 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeVariousInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/VisualTransformationDemo.kt
@@ -153,7 +153,7 @@
@Preview
@Composable
-fun VariousInputFieldDemo() {
+fun VisualTransformationDemo() {
LazyColumn {
item {
TagLine(tag = "Capitalization")
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
new file mode 100644
index 0000000..90fa1d7
--- /dev/null
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 android.os.Build
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.COMPILATION_MODES
+import androidx.testutils.measureStartup
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * This benchmark is used to verify that compilation mode performance is consistent across runs,
+ * and that compilation state doesn't leak across benchmarks.
+ */
+@LargeTest
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+@RunWith(Parameterized::class)
+class CompilationConsistencyBenchmark(
+ @Suppress("UNUSED_PARAMETER") iteration: Int,
+ private val compilationMode: CompilationMode
+) {
+ @get:Rule
+ val benchmarkRule = MacrobenchmarkRule()
+
+ @Test
+ fun startup() = benchmarkRule.measureStartup(
+ compilationMode = compilationMode,
+ startupMode = StartupMode.COLD,
+ packageName = packageName
+ ) {
+ action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
+ putExtra("ITEM_COUNT", 5)
+ }
+
+ companion object {
+ val packageName = "androidx.compose.integration.macrobenchmark.target"
+
+ @Parameterized.Parameters(name = "iter={0},compilation={1}")
+ @JvmStatic
+ fun parameters(): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
+ for (iter in 1..4) {
+ for (compilationMode in COMPILATION_MODES) {
+ add(arrayOf(iter, compilationMode))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
index 1147a76..73e69950 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableBox.kt
@@ -70,7 +70,7 @@
)
.swipeAnchors(
state = swipeableState,
- possibleStates = possibleStates,
+ possibleValues = possibleStates,
calculateAnchor = calculateAnchor
)
.offset {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
index c7f0a55..6fb31971 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2AnchorTest.kt
@@ -56,14 +56,14 @@
lateinit var state: SwipeableV2State<TestState>
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
compositionCounter++
Box(
Modifier
.height(200.dp)
.swipeAnchors(
state,
- possibleStates = setOf(A, B, C)
+ possibleValues = setOf(A, B, C)
) { state, layoutSize ->
when (state) {
A -> 0f
@@ -92,13 +92,13 @@
val swipeableSize = 200.dp
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
Box(
Modifier
.requiredHeight(swipeableSize)
.swipeAnchors(
state,
- possibleStates = setOf(A, B, C)
+ possibleValues = setOf(A, B, C)
) { state, layoutSize ->
when (state) {
A -> 0f
@@ -131,7 +131,7 @@
.size(size.dp) // Trigger remeasure when size changes
.swipeAnchors(
state,
- possibleStates = setOf(A, B, C),
+ possibleValues = setOf(A, B, C),
calculateAnchor = { state, _ -> anchors[state] }
)
)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
index 551c9bc..a748549 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2GestureTest.kt
@@ -167,36 +167,36 @@
state.dispatchRawDelta(positionOfA + distance * (absThreshold * 0.9f))
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
state.dispatchRawDelta(distance * 0.2f)
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(B)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
state.dispatchRawDelta(-distance * (absThreshold * 0.9f))
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
state.dispatchRawDelta(-distance * 0.2f)
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(A)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
}
@Test
@@ -220,39 +220,39 @@
state.dispatchRawDelta(initialOffset + (absThreshold * 0.9f))
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
// Swipe towards B, close after threshold
state.dispatchRawDelta(absThreshold * 0.2f)
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(B)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
// Swipe towards A, close before threshold
state.dispatchRawDelta(-(absThreshold * 0.9f))
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
// Swipe towards A, close after threshold
state.dispatchRawDelta(-(absThreshold * 0.2f))
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(A)
runBlocking(AutoTestFrameClock()) { state.settle(velocity = 0f) }
- assertThat(state.currentState).isEqualTo(A)
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
}
@Test
@@ -273,7 +273,7 @@
}
state.dispatchRawDelta(60f)
state.settle(velocityPx)
- assertThat(state.currentState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
}
@Test
@@ -293,7 +293,7 @@
)
state.dispatchRawDelta(60f)
state.settle(velocityPx / 2)
- assertThat(state.currentState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
}
@Test
@@ -315,7 +315,7 @@
}
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
}
@Test
@@ -338,7 +338,7 @@
}
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(A)
+ assertThat(state.currentValue).isEqualTo(A)
}
private fun SwipeableTestState(
@@ -348,7 +348,7 @@
velocityThreshold: Dp = 125.dp,
anchors: Map<TestState, Float>? = null
) = SwipeableV2State(
- initialState = initialState,
+ initialValue = initialState,
positionalThreshold = positionalThreshold,
velocityThreshold = velocityThreshold,
density = density
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
index d7433e2..10da79f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/swipeable/SwipeableV2StateTest.kt
@@ -58,7 +58,7 @@
fun swipeable_state_canSkipStateByFling() {
lateinit var state: SwipeableV2State<TestState>
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
SwipeableBox(
swipeableState = state,
orientation = Orientation.Vertical,
@@ -71,14 +71,14 @@
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(C)
+ assertThat(state.currentValue).isEqualTo(C)
}
@Test
fun swipeable_targetState_updatedOnSwipe() {
lateinit var state: SwipeableV2State<TestState>
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
SwipeableBox(
swipeableState = state,
orientation = Orientation.Vertical,
@@ -89,17 +89,17 @@
rule.onNodeWithTag(swipeableTestTag)
.performTouchInput { swipeDown(endY = bottom * 0.45f) }
rule.waitForIdle()
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
rule.onNodeWithTag(swipeableTestTag)
.performTouchInput { swipeDown(endY = bottom * 0.9f) }
rule.waitForIdle()
- assertThat(state.targetState).isEqualTo(C)
+ assertThat(state.targetValue).isEqualTo(C)
rule.onNodeWithTag(swipeableTestTag)
.performTouchInput { swipeUp(endY = top * 1.1f) }
rule.waitForIdle()
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
}
@Test
@@ -110,7 +110,7 @@
lateinit var scope: CoroutineScope
rule.setContent {
state = rememberSwipeableV2State(
- initialState = A,
+ initialValue = A,
animationSpec = tween(animationDuration, easing = LinearEasing)
)
scope = rememberCoroutineScope()
@@ -122,24 +122,24 @@
}
scope.launch {
- state.animateTo(targetState = B)
+ state.animateTo(targetValue = B)
}
rule.mainClock.advanceTimeBy((animationDuration * 0.6).toLong())
assertWithMessage("Current state")
- .that(state.currentState)
+ .that(state.currentValue)
.isEqualTo(A)
assertWithMessage("Target state")
- .that(state.targetState)
+ .that(state.targetValue)
.isEqualTo(B)
rule.mainClock.advanceTimeBy((animationDuration * 0.4).toLong())
assertWithMessage("Current state")
- .that(state.currentState)
+ .that(state.currentValue)
.isEqualTo(B)
assertWithMessage("Target state")
- .that(state.targetState)
+ .that(state.targetValue)
.isEqualTo(B)
}
@@ -147,7 +147,7 @@
fun swipeable_progress_matchesSwipePosition() {
lateinit var state: SwipeableV2State<TestState>
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
WithTouchSlop(touchSlop = 0f) {
SwipeableBox(
swipeableState = state,
@@ -164,7 +164,7 @@
rule.onNodeWithTag(swipeableTestTag)
.performTouchInput { swipeDown(endY = almostAnchorB) }
- assertThat(state.targetState).isEqualTo(B)
+ assertThat(state.targetValue).isEqualTo(B)
assertThat(state.progress).isEqualTo(expectedProgress)
val almostAnchorA = anchorA + ((anchorB - anchorA) * 0.1f)
@@ -173,7 +173,7 @@
rule.onNodeWithTag(swipeableTestTag)
.performTouchInput { swipeUp(startY = anchorB, endY = almostAnchorA) }
- assertThat(state.targetState).isEqualTo(A)
+ assertThat(state.targetValue).isEqualTo(A)
assertThat(state.progress).isEqualTo(expectedProgress)
}
@@ -181,7 +181,7 @@
fun swipeable_snapTo_updatesImmediately() = runBlocking {
lateinit var state: SwipeableV2State<TestState>
rule.setContent {
- state = rememberSwipeableV2State(initialState = A)
+ state = rememberSwipeableV2State(initialValue = A)
SwipeableBox(
swipeableState = state,
orientation = Orientation.Vertical
@@ -189,7 +189,7 @@
}
state.snapTo(C)
- assertThat(state.currentState)
+ assertThat(state.currentValue)
.isEqualTo(C)
}
@@ -210,26 +210,26 @@
restorationTester.emulateSavedInstanceStateRestore()
- assertThat(state.currentState).isEqualTo(initialState)
+ assertThat(state.currentValue).isEqualTo(initialState)
assertThat(state.animationSpec).isEqualTo(animationSpec)
scope.launch {
state.animateTo(B)
}
rule.waitForIdle()
- assertThat(state.currentState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
restorationTester.emulateSavedInstanceStateRestore()
- assertThat(state.currentState).isEqualTo(B)
+ assertThat(state.currentValue).isEqualTo(B)
}
@Test
fun swipeable_targetState_accessedInInitialComposition() {
lateinit var targetState: TestState
rule.setContent {
- val state = rememberSwipeableV2State(initialState = B)
- LaunchedEffect(state.targetState) {
- targetState = state.targetState
+ val state = rememberSwipeableV2State(initialValue = B)
+ LaunchedEffect(state.targetValue) {
+ targetState = state.targetValue
}
SwipeableBox(state)
}
@@ -241,7 +241,7 @@
fun swipeable_progress_accessedInInitialComposition() {
var progress = Float.NaN
rule.setContent {
- val state = rememberSwipeableV2State(initialState = B)
+ val state = rememberSwipeableV2State(initialValue = B)
LaunchedEffect(state.progress) {
progress = state.progress
}
@@ -258,7 +258,7 @@
lateinit var state: SwipeableV2State<TestState>
var offset: Float? = null
rule.setContent {
- state = rememberSwipeableV2State(initialState = B)
+ state = rememberSwipeableV2State(initialValue = B)
SwipeableBox(state)
exception = runCatching { offset = state.requireOffset() }.exceptionOrNull()
}
@@ -275,7 +275,7 @@
fun swipeable_requireOffset_accessedInEffect_doesntThrow() {
var exception: Throwable? = null
rule.setContent {
- val state = rememberSwipeableV2State(initialState = B)
+ val state = rememberSwipeableV2State(initialValue = B)
LaunchedEffect(Unit) {
exception = runCatching { state.requireOffset() }.exceptionOrNull()
}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
index b21f1bc..a9df38a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/SwipeableV2.kt
@@ -44,12 +44,12 @@
import kotlinx.coroutines.launch
/**
- * Enable swipe gestures between a set of predefined states.
+ * Enable swipe gestures between a set of predefined values.
*
* When a swipe is detected, the offset of the [SwipeableV2State] will be updated with the swipe
* delta. You should use this offset to move your content accordingly (see [Modifier.offset]).
* When the swipe ends, the offset will be animated to one of the anchors and when that anchor is
- * reached, the value of the [SwipeableV2State] will also be updated to the state corresponding to
+ * reached, the value of the [SwipeableV2State] will also be updated to the value corresponding to
* the new anchor.
*
* Swiping is constrained between the minimum and maximum anchors.
@@ -84,24 +84,24 @@
* the state with them.
*
* @param state The associated [SwipeableV2State]
- * @param possibleStates All possible states the [SwipeableV2State] could be in.
+ * @param possibleValues All possible values the [SwipeableV2State] could be in.
* @param anchorsChanged A callback to be invoked when the anchors have changed, `null` by default.
* Components with custom reconciliation logic should implement this callback, i.e. to re-target an
* in-progress animation.
* @param calculateAnchor This method will be invoked to calculate the position of all
- * [possibleStates], given this node's layout size. Return the anchor's offset from the initial
- * anchor, or `null` to indicate that a state does not exist.
+ * [possibleValues], given this node's layout size. Return the anchor's offset from the initial
+ * anchor, or `null` to indicate that a value does not have an anchor.
*/
@ExperimentalMaterialApi
internal fun <T> Modifier.swipeAnchors(
state: SwipeableV2State<T>,
- possibleStates: Set<T>,
+ possibleValues: Set<T>,
anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
- calculateAnchor: (state: T, layoutSize: IntSize) -> Float?,
+ calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
) = onSizeChanged { layoutSize ->
val previousAnchors = state.anchors
val newAnchors = mutableMapOf<T, Float>()
- possibleStates.forEach {
+ possibleValues.forEach {
val anchorValue = calculateAnchor(it, layoutSize)
if (anchorValue != null) {
newAnchors[it] = anchorValue
@@ -122,10 +122,10 @@
* to change the state either immediately or by starting an animation. To create and remember a
* [SwipeableV2State] use [rememberSwipeableV2State].
*
- * @param initialState The initial value of the state.
+ * @param initialValue The initial value of the state.
* @param density The density used to convert thresholds from px to dp.
* @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
* @param positionalThreshold The positional threshold to be used when calculating the target state
* while a swipe is in progress and when settling after the swipe ends. This is the distance from
* the start of a transition. It will be, depending on the direction of the interaction, added or
@@ -138,31 +138,31 @@
@Stable
@ExperimentalMaterialApi
internal class SwipeableV2State<T>(
- initialState: T,
+ initialValue: T,
internal val density: Density,
internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- internal val confirmStateChange: (newValue: T) -> Boolean = { true },
+ internal val confirmValueChange: (newValue: T) -> Boolean = { true },
internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
SwipeableV2Defaults.PositionalThreshold,
internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
) {
/**
- * The current state of the [SwipeableV2State].
+ * The current value of the [SwipeableV2State].
*/
- var currentState: T by mutableStateOf(initialState)
+ var currentValue: T by mutableStateOf(initialValue)
private set
/**
- * The target state. This is the closest state to the current offset (taking into account
+ * The target value. This is the closest value to the current offset (taking into account
* positional thresholds). If no interactions like animations or drags are in progress, this
- * will be the current state.
+ * will be the current value.
*/
- val targetState: T by derivedStateOf {
+ val targetValue: T by derivedStateOf {
val currentOffset = offset
if (currentOffset != null) {
- computeTarget(currentOffset, currentState, velocity = 0f)
- } else currentState
+ computeTarget(currentOffset, currentValue, velocity = 0f)
+ } else currentValue
}
/**
@@ -199,12 +199,13 @@
private set
/**
- * The fraction of the progress going from currentState to targetState, within [0f..1f] bounds.
+ * The fraction of the progress going from [currentValue] to [targetValue], within [0f..1f]
+ * bounds.
*/
/*@FloatRange(from = 0f, to = 1f)*/
val progress: Float by derivedStateOf {
- val a = anchors[currentState] ?: 0f
- val b = anchors[targetState] ?: 0f
+ val a = anchors[currentValue] ?: 0f
+ val b = anchors[targetValue] ?: 0f
val distance = abs(b - a)
if (distance > 1e-6f) {
val progress = (this.requireOffset() - a) / (b - a)
@@ -239,45 +240,45 @@
val previousAnchorsEmpty = anchors.isEmpty()
anchors = newAnchors
if (previousAnchorsEmpty) {
- dragPosition = anchors.requireAnchor(this.currentState)
+ dragPosition = anchors.requireAnchor(this.currentValue)
}
}
/**
- * Whether the [state] has an anchor associated with it.
+ * Whether the [value] has an anchor associated with it.
*/
- fun hasAnchorForState(state: T): Boolean = anchors.containsKey(state)
+ fun hasAnchorForValue(value: T): Boolean = anchors.containsKey(value)
/**
- * Snap to a [targetState] without any animation.
+ * Snap to a [targetValue] without any animation.
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
*/
- suspend fun snapTo(targetState: T) {
- val targetOffset = anchors.requireAnchor(targetState)
+ suspend fun snapTo(targetValue: T) {
+ val targetOffset = anchors.requireAnchor(targetValue)
draggableState.drag {
dragBy(targetOffset - requireOffset())
}
- this.currentState = targetState
+ this.currentValue = targetValue
}
/**
- * Animate to a [targetState].
+ * Animate to a [targetValue].
*
* @throws CancellationException if the interaction interrupted by another interaction like a
* gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
*
- * @param targetState The target state of the animation
+ * @param targetValue The target value of the animation
* @param velocity The velocity the animation should start with, [lastVelocity] by default
*/
suspend fun animateTo(
- targetState: T,
+ targetValue: T,
velocity: Float = lastVelocity,
) {
- val targetOffset = anchors.requireAnchor(targetState)
+ val targetOffset = anchors.requireAnchor(targetValue)
try {
draggableState.drag {
isAnimationRunning = true
@@ -302,7 +303,7 @@
.entries
.firstOrNull { (_, anchorOffset) -> abs(anchorOffset - endOffset) < 0.5f }
?.key
- this.currentState = endState ?: currentState
+ this.currentValue = endState ?: currentValue
}
}
@@ -310,17 +311,17 @@
* Find the closest anchor taking into account the velocity and settle at it with an animation.
*/
suspend fun settle(velocity: Float) {
- val previousState = this.currentState
- val targetState = computeTarget(
+ val previousValue = this.currentValue
+ val targetValue = computeTarget(
offset = requireOffset(),
- currentState = previousState,
+ currentValue = previousValue,
velocity = velocity
)
- if (confirmStateChange(targetState)) {
- animateTo(targetState, velocity)
+ if (confirmValueChange(targetValue)) {
+ animateTo(targetValue, velocity)
} else {
// If the user vetoed the state change, rollback to the previous state.
- animateTo(previousState, velocity)
+ animateTo(previousValue, velocity)
}
}
@@ -342,32 +343,32 @@
private fun computeTarget(
offset: Float,
- currentState: T,
+ currentValue: T,
velocity: Float
): T {
val currentAnchors = anchors
- val currentAnchor = currentAnchors.requireAnchor(currentState)
+ val currentAnchor = currentAnchors.requireAnchor(currentValue)
return if (currentAnchor <= offset) {
// Swiping from lower to upper (positive).
if (velocity >= velocityThresholdPx) {
- currentAnchors.closestState(offset, true)
+ currentAnchors.closestAnchor(offset, true)
} else {
- val upper = currentAnchors.closestState(offset, true)
+ val upper = currentAnchors.closestAnchor(offset, true)
val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
val relativeThreshold = abs(positionalThreshold(density, distance))
val absoluteThreshold = abs(currentAnchor + relativeThreshold)
- if (offset < absoluteThreshold) currentState else upper
+ if (offset < absoluteThreshold) currentValue else upper
}
} else {
// Swiping from upper to lower (negative).
if (velocity <= -velocityThresholdPx) {
- currentAnchors.closestState(offset, false)
+ currentAnchors.closestAnchor(offset, false)
} else {
- val lower = currentAnchors.closestState(offset, false)
+ val lower = currentAnchors.closestAnchor(offset, false)
val distance = abs(currentAnchor - currentAnchors.getValue(lower))
val relativeThreshold = abs(positionalThreshold(density, distance))
val absoluteThreshold = abs(currentAnchor - relativeThreshold)
- if (offset > absoluteThreshold) currentState else lower
+ if (offset > absoluteThreshold) currentValue else lower
}
}
}
@@ -379,17 +380,17 @@
@ExperimentalMaterialApi
fun <T : Any> Saver(
animationSpec: AnimationSpec<Float>,
- confirmStateChange: (T) -> Boolean,
+ confirmValueChange: (T) -> Boolean,
positionalThreshold: Density.(distance: Float) -> Float,
velocityThreshold: Dp,
density: Density
) = Saver<SwipeableV2State<T>, T>(
- save = { it.currentState },
+ save = { it.currentValue },
restore = {
SwipeableV2State(
- initialState = it,
+ initialValue = it,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = positionalThreshold,
velocityThreshold = velocityThreshold,
density = density
@@ -402,32 +403,32 @@
/**
* Create and remember a [SwipeableV2State].
*
- * @param initialState The initial state.
- * @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param initialValue The initial value.
+ * @param animationSpec The default animation that will be used to animate to a new value.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending value change.
*/
@Composable
@ExperimentalMaterialApi
internal fun <T : Any> rememberSwipeableV2State(
- initialState: T,
+ initialValue: T,
animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
- confirmStateChange: (newValue: T) -> Boolean = { true }
+ confirmValueChange: (newValue: T) -> Boolean = { true }
): SwipeableV2State<T> {
val density = LocalDensity.current
return rememberSaveable(
- initialState, animationSpec, confirmStateChange, density,
+ initialValue, animationSpec, confirmValueChange, density,
saver = SwipeableV2State.Saver(
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
density = density
),
) {
SwipeableV2State(
- initialState = initialState,
+ initialValue = initialValue,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange,
+ confirmValueChange = confirmValueChange,
positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
density = density
@@ -484,11 +485,11 @@
fixedPositionalThreshold(56.dp)
}
-private fun <T> Map<T, Float>.closestState(
+private fun <T> Map<T, Float>.closestAnchor(
offset: Float = 0f,
searchUpwards: Boolean = false
): T {
- require(isNotEmpty()) { "The anchors were empty when trying to find the closest state" }
+ require(isNotEmpty()) { "The anchors were empty when trying to find the closest anchor" }
return minBy { (_, anchor) ->
val delta = if (searchUpwards) anchor - offset else offset - anchor
if (delta < 0) Float.POSITIVE_INFINITY else delta
@@ -497,6 +498,6 @@
private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(state: T) = requireNotNull(this[state]) {
- "Required anchor $state was not found in anchors. Current anchors: ${this.toMap()}"
+private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
+ "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt
new file mode 100644
index 0000000..6cc751a
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchBarTokens.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SearchBarTokens {
+ val AvatarShape = ShapeKeyTokens.CornerFull
+ val AvatarSize = 30.0.dp
+ val ContainerColor = ColorSchemeKeyTokens.Surface
+ val ContainerElevation = ElevationTokens.Level3
+ val ContainerHeight = 56.0.dp
+ val ContainerShape = ShapeKeyTokens.CornerFull
+ val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+ val HoverSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val InputTextColor = ColorSchemeKeyTokens.OnSurface
+ val InputTextFont = TypographyKeyTokens.BodyLarge
+ val LeadingIconColor = ColorSchemeKeyTokens.OnSurface
+ val PressedSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val SupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val SupportingTextFont = TypographyKeyTokens.BodyLarge
+ val TrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt
new file mode 100644
index 0000000..ae626cf
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/SearchViewTokens.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object SearchViewTokens {
+ val ContainerColor = ColorSchemeKeyTokens.Surface
+ val ContainerElevation = ElevationTokens.Level3
+ val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+ val DividerColor = ColorSchemeKeyTokens.Outline
+ val DockedContainerShape = ShapeKeyTokens.CornerExtraLarge
+ val DockedHeaderContainerHeight = 56.0.dp
+ val FullScreenContainerShape = ShapeKeyTokens.CornerNone
+ val FullScreenHeaderContainerHeight = 72.0.dp
+ val HeaderInputTextColor = ColorSchemeKeyTokens.OnSurface
+ val HeaderInputTextFont = TypographyKeyTokens.BodyLarge
+ val HeaderLeadingIconColor = ColorSchemeKeyTokens.OnSurface
+ val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+ val HeaderSupportingTextFont = TypographyKeyTokens.BodyLarge
+ val HeaderTrailingIconColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
\ No newline at end of file
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 48e2c23..e637a6a 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
@@ -147,59 +147,6 @@
}
/**
- * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions.
- * Its cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown].
- */
- private val effectJob = Job(effectCoroutineContext[Job]).apply {
- invokeOnCompletion { throwable ->
- // Since the running recompose job is operating in a disjoint job if present,
- // kick it out and make sure no new ones start if we have one.
- val cancellation = CancellationException("Recomposer effect job completed", throwable)
-
- var continuationToResume: CancellableContinuation<Unit>? = null
- synchronized(stateLock) {
- val runnerJob = runnerJob
- if (runnerJob != null) {
- _state.value = State.ShuttingDown
- // If the recomposer is closed we will let the runnerJob return from
- // runRecomposeAndApplyChanges normally and consider ourselves shut down
- // immediately.
- if (!isClosed) {
- // This is the job hosting frameContinuation; no need to resume it otherwise
- runnerJob.cancel(cancellation)
- } else if (workContinuation != null) {
- continuationToResume = workContinuation
- }
- workContinuation = null
- runnerJob.invokeOnCompletion { runnerJobCause ->
- synchronized(stateLock) {
- closeCause = throwable?.apply {
- runnerJobCause
- ?.takeIf { it !is CancellationException }
- ?.let { addSuppressed(it) }
- }
- _state.value = State.ShutDown
- }
- }
- } else {
- closeCause = cancellation
- _state.value = State.ShutDown
- }
- }
- continuationToResume?.resume(Unit)
- }
- }
-
- /**
- * The [effectCoroutineContext] is derived from the parameter of the same name.
- */
- internal override val effectCoroutineContext: CoroutineContext =
- effectCoroutineContext + broadcastFrameClock + effectJob
-
- internal override val recomposeCoroutineContext: CoroutineContext
- get() = EmptyCoroutineContext
-
- /**
* Valid operational states of a [Recomposer].
*/
enum class State {
@@ -271,6 +218,63 @@
private val _state = MutableStateFlow(State.Inactive)
/**
+ * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions.
+ * Its cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown].
+ *
+ * Initialized after other state above, since it is possible for [Job.invokeOnCompletion]
+ * to run synchronously during construction if the [Recomposer] is constructed with
+ * a completed or cancelled [Job].
+ */
+ private val effectJob = Job(effectCoroutineContext[Job]).apply {
+ invokeOnCompletion { throwable ->
+ // Since the running recompose job is operating in a disjoint job if present,
+ // kick it out and make sure no new ones start if we have one.
+ val cancellation = CancellationException("Recomposer effect job completed", throwable)
+
+ var continuationToResume: CancellableContinuation<Unit>? = null
+ synchronized(stateLock) {
+ val runnerJob = runnerJob
+ if (runnerJob != null) {
+ _state.value = State.ShuttingDown
+ // If the recomposer is closed we will let the runnerJob return from
+ // runRecomposeAndApplyChanges normally and consider ourselves shut down
+ // immediately.
+ if (!isClosed) {
+ // This is the job hosting frameContinuation; no need to resume it otherwise
+ runnerJob.cancel(cancellation)
+ } else if (workContinuation != null) {
+ continuationToResume = workContinuation
+ }
+ workContinuation = null
+ runnerJob.invokeOnCompletion { runnerJobCause ->
+ synchronized(stateLock) {
+ closeCause = throwable?.apply {
+ runnerJobCause
+ ?.takeIf { it !is CancellationException }
+ ?.let { addSuppressed(it) }
+ }
+ _state.value = State.ShutDown
+ }
+ }
+ } else {
+ closeCause = cancellation
+ _state.value = State.ShutDown
+ }
+ }
+ continuationToResume?.resume(Unit)
+ }
+ }
+
+ /**
+ * The [effectCoroutineContext] is derived from the parameter of the same name.
+ */
+ internal override val effectCoroutineContext: CoroutineContext =
+ effectCoroutineContext + broadcastFrameClock + effectJob
+
+ internal override val recomposeCoroutineContext: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ /**
* Determine the new value of [_state]. Call only while locked on [stateLock].
* If it returns a continuation, that continuation should be resumed after releasing the lock.
*/
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
index 0a2bbe1..65078df 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/RecomposerTests.kt
@@ -304,6 +304,11 @@
assertEquals(1, recompostions)
}
+
+ @Test
+ fun constructRecomposerWithCancelledJob() {
+ Recomposer(Job().apply { cancel() })
+ }
}
class UnitApplier : Applier<Unit> {
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 72e371e..99ae536 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -73,6 +73,7 @@
androidTestImplementation(libs.mockitoCore)
androidTestImplementation(libs.dexmakerMockito)
androidTestImplementation(libs.mockitoKotlin)
+ androidTestImplementation(libs.kotlinTest)
samples(project(":compose:ui:ui-test:ui-test-samples"))
}
@@ -133,6 +134,7 @@
implementation(libs.mockitoCore)
implementation(libs.mockitoKotlin)
implementation(libs.dexmakerMockito)
+ implementation(libs.kotlinTest)
}
desktopMain.dependencies {
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
new file mode 100644
index 0000000..8730103
--- /dev/null
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PhaseOrderingTest.kt
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test
+
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.util.TestCounter
+import androidx.test.filters.SmallTest
+import kotlinx.coroutines.launch
+import org.junit.Rule
+import org.junit.Test
+
+@SmallTest
+class PhaseOrderingTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun singlePass() {
+ val counter = TestCounter()
+ rule.setContent {
+ // This should never recompose.
+ counter.expect(1)
+
+ LaunchedEffect(Unit) {
+ counter.expect(2)
+ withFrameNanos {
+ counter.expect(6)
+ launch {
+ // TODO(b/254115946) No continuations resumed during a frame should be
+ // dispatched until after the frame callbacks finish running.
+ counter.expect(7)
+ }
+ counter.expect(8)
+ }
+ counter.expect(9)
+ }
+
+ Layout(
+ content = {},
+ modifier = Modifier.drawBehind {
+ counter.expect(5)
+ }
+ ) { _, _ ->
+ counter.expect(3)
+ layout(1, 1) {
+ counter.expect(4)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun frameCallbackRestartsLayout() {
+ val counter = TestCounter()
+ var firstPass = true
+
+ rule.setContent {
+ LaunchedEffect(Unit) {
+ counter.expect(1)
+ withFrameNanos {
+ // This won't run until the 2nd frame, so the first layout happens before it.
+ counter.expect(4)
+ firstPass = false
+ }
+ // TODO(b/222093277) This should happen *after* the next layout pass.
+ counter.expect(5)
+ }
+
+ Layout(content = {}) { _, _ ->
+ // TODO(b/222093277) This should happen *before* the next layout pass.
+ counter.expect(if (firstPass) 2 else 6)
+ layout(1, 1) {
+ counter.expect(3)
+ }
+ }
+ }
+ }
+}
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
index 60bf60c..22f51cb 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/TestMonotonicFrameClockTest.kt
@@ -17,57 +17,307 @@
package androidx.compose.ui.test
import androidx.compose.runtime.withFrameNanos
+import androidx.compose.ui.test.util.TestCounter
import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.currentTime
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
import org.junit.Assert.assertEquals
-import org.junit.Assert.fail
import org.junit.Test
-@ExperimentalCoroutinesApi
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class TestMonotonicFrameClockTest {
+
+ // These delay tests aren't just testing that the test dispatchers correctly skip delays
+ // (which is code not under our control and thus not worth testing), but rather that the
+ // TestMonotonicFrameClock machinery doesn't break the delay mechanism and end up using wall
+ // clock time for delays.
@Test
- fun testMonotonicFrameClockRunsFrame() = runTest(UnconfinedTestDispatcher()) {
- val frameDelayNanos = 16_000_000L
- withContext(TestMonotonicFrameClock(this, frameDelayNanos)) {
- val startTime = currentTime
- val expectedFrameTime = startTime + frameDelayNanos / 1_000_000
- val counter = TestCounter()
+ fun delaysAreSkipped_unconfinedDispatcher() = runTest(UnconfinedTestDispatcher()) {
+ val delayMillis = 999_999_999L
+ withTestClockContext {
+ val virtualStartTimeMillis = currentTime
+ val realStartTimeNanos = System.nanoTime()
+ delay(delayMillis)
+ val realDurationMillis = (System.nanoTime() - realStartTimeNanos) / 1_000_000
+ val virtualDurationMillis = currentTime - virtualStartTimeMillis
+
+ assertThat(virtualDurationMillis).isEqualTo(delayMillis)
+ // This could theoretically fail if the test host is running _really_ slow, but 500ms
+ // should be more than enough.
+ assertThat(realDurationMillis).isLessThan(500)
+ }
+ }
+
+ @Test
+ fun delaysAreSkipped_standardDispatcher() = runTest(StandardTestDispatcher()) {
+ val delayMillis = 999_999_999L
+ withTestClockContext {
+ val virtualStartTimeMillis = currentTime
+ val realStartTimeNanos = System.nanoTime()
+ delay(delayMillis)
+ val realDurationMillis = (System.nanoTime() - realStartTimeNanos) / 1_000_000
+ val virtualDurationMillis = currentTime - virtualStartTimeMillis
+
+ assertThat(virtualDurationMillis).isEqualTo(delayMillis)
+ // This could theoretically fail if the test host is running _really_ slow, but 500ms
+ // should be more than enough.
+ assertThat(realDurationMillis).isLessThan(500)
+ }
+ }
+
+ @Test
+ fun testMonotonicFrameClockRunsFrame_unconfinedDispatcher() =
+ runTest(UnconfinedTestDispatcher()) {
+ withTestClockContext {
+ val startTime = currentTime
+ val expectedFrameTime = startTime + FrameDelayNanos / 1_000_000
+ val counter = TestCounter()
+ launch {
+ counter.expect(1, "in coroutine 1")
+ withFrameNanos {
+ counter.expect(4, "in frame callback 1")
+ assertEquals("frame time 1", expectedFrameTime, currentTime)
+ }
+ counter.expect(6, "after resuming frame callback 1")
+ }
+ launch {
+ counter.expect(2, "in coroutine 2")
+ withFrameNanos {
+ counter.expect(5, "in frame callback 2")
+ assertEquals("frame time 2", expectedFrameTime, currentTime)
+ }
+ counter.expect(7, "after resuming frame callback 2")
+ }
+ counter.expect(3)
+ advanceUntilIdleWorkaround()
+ counter.expect(8, "final result")
+ }
+ }
+
+ @Test
+ fun testMonotonicFrameClockRunsFrame_standardDispatcher() =
+ runTest(StandardTestDispatcher()) {
+ withTestClockContext {
+ val startTime = currentTime
+ val expectedFrameTime = startTime + FrameDelayNanos / 1_000_000
+ val counter = TestCounter()
+ launch {
+ counter.expect(2, "in coroutine 1")
+ withFrameNanos {
+ counter.expect(4, "in frame callback 1")
+ assertEquals("frame time 1", expectedFrameTime, currentTime)
+ }
+ counter.expect(6, "after resuming frame callback 1")
+ }
+ launch {
+ counter.expect(3, "in coroutine 2")
+ withFrameNanos {
+ counter.expect(5, "in frame callback 2")
+ assertEquals("frame time 2", expectedFrameTime, currentTime)
+ }
+ counter.expect(7, "after resuming frame callback 2")
+ }
+ counter.expect(1)
+ advanceUntilIdleWorkaround()
+ counter.expect(8, "final result")
+ }
+ }
+
+ @Test
+ fun testMonotonicFrameClockDispatcherDefersResumesInsideFrame_unconfinedDispatcher() =
+ runTest(UnconfinedTestDispatcher()) {
+ withTestClockContext {
+ val counter = TestCounter()
+ var continuation: Continuation<Unit>? = null
+
+ launch {
+ counter.expect(1, "in external coroutine")
+ suspendCancellableCoroutine { continuation = it }
+ counter.expect(9, "after resuming external coroutine")
+ }
+
+ launch {
+ counter.expect(2)
+ withFrameNanos {
+ counter.expect(5, "in frame callback 1")
+ // Coroutines launched inside withFrameNanos shouldn't be dispatched until
+ // after all frame callbacks are complete.
+ launch {
+ counter.expect(6, "in \"effect\" coroutine")
+ }
+ counter.expect(7, "after launching \"effect\" coroutine")
+ }
+ counter.expect(11, "after resuming frame callback 1")
+ }
+
+ launch {
+ counter.expect(3)
+ withFrameNanos {
+ counter.expect(8, "in frame callback 2")
+ // Coroutines that were already suspended and are resumed inside
+ // withFrameNanos shouldn't be dispatched until after all frame callbacks
+ // are complete either.
+ continuation!!.resume(Unit)
+ counter.expect(10, "after resuming external continuation")
+ }
+ counter.expect(12, "after resuming frame callback 2")
+ }
+
+ counter.expect(4)
+ advanceUntilIdleWorkaround()
+ counter.expect(13, "final result")
+ }
+ }
+
+ @Test
+ fun testMonotonicFrameClockDispatcherDefersResumesInsideFrame_standardDispatcher() =
+ runTest(StandardTestDispatcher()) {
+ withTestClockContext {
+ val counter = TestCounter()
+ var continuation: Continuation<Unit>? = null
+
+ launch {
+ counter.expect(2, "in external coroutine")
+ suspendCancellableCoroutine { continuation = it }
+ counter.expect(10, "after resuming external coroutine")
+ }
+
+ launch {
+ counter.expect(3)
+ withFrameNanos {
+ counter.expect(5, "in frame callback 1")
+ // Coroutines launched inside withFrameNanos shouldn't be dispatched until
+ // after all frame callbacks are complete.
+ launch {
+ counter.expect(9, "in \"effect\" coroutine")
+ }
+ counter.expect(6, "after launching \"effect\" coroutine")
+ }
+ counter.expect(11, "after resuming frame callback 1")
+ }
+
+ launch {
+ counter.expect(4)
+ withFrameNanos {
+ counter.expect(7, "in frame callback 2")
+ // Coroutines that were already suspended and are resumed inside
+ // withFrameNanos shouldn't be dispatched until after all frame callbacks
+ // are complete either.
+ continuation!!.resume(Unit)
+ counter.expect(8, "after resuming external continuation")
+ }
+ counter.expect(12, "after resuming frame callback 2")
+ }
+
+ counter.expect(1)
+ advanceUntilIdleWorkaround()
+ counter.expect(13, "final result")
+ }
+ }
+
+ @Test
+ fun withFrameNanosThrows_unconfinedDispatcher() = runTest(UnconfinedTestDispatcher()) {
+ val message = "the frame threw an error"
+ var error: RuntimeException? = null
+ var firstCoroutineContinued = false
+ var secondFrameCallbackRan = false
+ var secondCoroutineContinued = false
+
+ withTestClockContext {
+ launch {
+ try {
+ withFrameNanos {
+ throw RuntimeException(message)
+ }
+ } catch (e: RuntimeException) {
+ error = e
+ }
+ firstCoroutineContinued = true
+ }
+
launch {
withFrameNanos {
- counter.expect(2, "in frame callback 1")
- assertEquals("frame time 1", expectedFrameTime, currentTime)
+ // If one frame callback throws, other frame callbacks should still run.
+ secondFrameCallbackRan = true
}
- counter.expect(4, "after resuming frame callback 1")
+ secondCoroutineContinued = true
}
+
+ advanceUntilIdleWorkaround()
+ assertThat(error).hasMessageThat().isEqualTo(message)
+ assertThat(firstCoroutineContinued).isTrue()
+ assertThat(secondFrameCallbackRan).isTrue()
+ assertThat(secondCoroutineContinued).isTrue()
+ }
+ }
+
+ @Test
+ fun withFrameNanosThrows_standardDispatcher() = runTest(StandardTestDispatcher()) {
+ val message = "the frame threw an error"
+ var error: RuntimeException? = null
+ var firstCoroutineContinued = false
+ var secondFrameCallbackRan = false
+ var secondCoroutineContinued = false
+
+ withTestClockContext {
+ launch {
+ try {
+ withFrameNanos {
+ throw RuntimeException(message)
+ }
+ } catch (e: RuntimeException) {
+ error = e
+ }
+ firstCoroutineContinued = true
+ }
+
launch {
withFrameNanos {
- counter.expect(3, "in frame callback 2")
- assertEquals("frame time 2", expectedFrameTime, currentTime)
+ // If one frame callback throws, other frame callbacks should still run.
+ secondFrameCallbackRan = true
}
- counter.expect(5, "after resuming frame callback 2")
+ secondCoroutineContinued = true
}
- counter.expect(1)
+
+ advanceUntilIdleWorkaround()
+ assertThat(error).hasMessageThat().isEqualTo(message)
+ assertThat(firstCoroutineContinued).isTrue()
+ assertThat(secondFrameCallbackRan).isTrue()
+ assertThat(secondCoroutineContinued).isTrue()
+ }
+ }
+
+ private suspend fun TestScope.withTestClockContext(block: suspend CoroutineScope.() -> Unit) {
+ withContext(TestMonotonicFrameClock(this, FrameDelayNanos), block)
+ }
+
+ /** Workaround for https://github.com/Kotlin/kotlinx.coroutines/issues/3493. */
+ private suspend fun TestScope.advanceUntilIdleWorkaround() {
+ // Yielding is required to dispatch launches.
+ // Advancing is required to execute withFrameNanos callbacks.
+ // There need to be enough repetitions to execute all of each of those in the tests.
+ repeat(10) {
+ yield()
advanceUntilIdle()
- counter.expect(6, "final result")
}
}
-}
-private class TestCounter {
- private var count = 0
-
- fun expect(checkpoint: Int, message: String = "(no message)") {
- val expected = count + 1
- if (checkpoint != expected) {
- fail("out of order event $checkpoint, expected $expected, $message")
- }
- count = expected
+ private companion object {
+ const val FrameDelayNanos = 16_000_000L
}
-}
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
new file mode 100644
index 0000000..9fac117
--- /dev/null
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/util/TestCounter.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.test.util
+
+import org.junit.Assert.fail
+
+/**
+ * Asserts that [expect] calls occur in a specific order. Useful for coroutine dispatching tests.
+ */
+internal class TestCounter {
+ private var count = 0
+
+ fun expect(checkpoint: Int, message: String = "(no message)") {
+ val expected = count + 1
+ if (checkpoint != expected) {
+ fail("out of order event $checkpoint, expected $expected, $message")
+ }
+ count = expected
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 73ffd26..8b2d4ee 100644
--- a/compose/ui/ui-tooling/api/current.txt
+++ b/compose/ui/ui-tooling/api/current.txt
@@ -8,6 +8,9 @@
method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class PreviewLogger_androidKt {
+ }
+
public final class PreviewUtilsKt {
}
@@ -21,6 +24,13 @@
public final class AnimationSearchKt {
}
+ public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+ ctor public ToolingState(T? default);
+ method public T! getValue();
+ method public void setValue(T!);
+ property public T! value;
+ }
+
public final class TransitionComposeAnimationKt {
}
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index a778a63..cfd82b6 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -13,6 +13,9 @@
method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class PreviewLogger_androidKt {
+ }
+
public final class PreviewUtilsKt {
}
@@ -26,6 +29,13 @@
public final class AnimationSearchKt {
}
+ public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+ ctor public ToolingState(T? default);
+ method public T! getValue();
+ method public void setValue(T!);
+ property public T! value;
+ }
+
public final class TransitionComposeAnimationKt {
}
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 73ffd26..8b2d4ee 100644
--- a/compose/ui/ui-tooling/api/restricted_current.txt
+++ b/compose/ui/ui-tooling/api/restricted_current.txt
@@ -8,6 +8,9 @@
method @Deprecated @androidx.compose.runtime.Composable public static void InInspectionModeOnly(kotlin.jvm.functions.Function0<kotlin.Unit> content);
}
+ public final class PreviewLogger_androidKt {
+ }
+
public final class PreviewUtilsKt {
}
@@ -21,6 +24,13 @@
public final class AnimationSearchKt {
}
+ public final class ToolingState<T> implements androidx.compose.runtime.State<T> {
+ ctor public ToolingState(T? default);
+ method public T! getValue();
+ method public void setValue(T!);
+ property public T! value;
+ }
+
public final class TransitionComposeAnimationKt {
}
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
new file mode 100644
index 0000000..e1233ef
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeInvokerTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.currentComposer
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import java.lang.reflect.InvocationTargetException
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+
+@Suppress("DEPRECATION")
+@OptIn(ExperimentalComposeUiApi::class)
+class ComposeInvokerTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun workingComposable() {
+ rule.setContent {
+ ComposableInvoker.invokeComposable(
+ "androidx.compose.ui.tooling.MyTestComposables",
+ "MyWorkingComposable",
+ currentComposer
+ )
+ }
+ }
+
+ @Test
+ fun composableClassNotFound() {
+ try {
+ rule.setContent {
+ ComposableInvoker.invokeComposable(
+ "androidx.compose.ui.tooling.ClassDoesntExist",
+ "MyWorkingComposable",
+ currentComposer
+ )
+ }
+ fail("ClassNotFoundException expected to be thrown")
+ } catch (e: ClassNotFoundException) {
+ // ClassNotFoundException expected to be thrown when Composable class is not found.
+ }
+ }
+
+ @Test
+ fun composableMethodNotFound() {
+ try {
+ rule.setContent {
+ ComposableInvoker.invokeComposable(
+ "androidx.compose.ui.tooling.MyTestComposables",
+ "MethodDoesntExist",
+ currentComposer
+ )
+ }
+ fail("NoSuchMethodException expected to be thrown")
+ } catch (e: NoSuchMethodException) {
+ // NoSuchMethodException expected to be thrown when Composable method is not found.
+ }
+ }
+
+ @Test
+ fun composableMethodThrowsException() {
+ try {
+ rule.setContent {
+ ComposableInvoker.invokeComposable(
+ "androidx.compose.ui.tooling.MyTestComposables",
+ "MyThrowExceptionComposable",
+ currentComposer
+ )
+ }
+ fail("InvocationTargetException expected to be thrown")
+ } catch (e: InvocationTargetException) {
+ // InvocationTargetException expected to be thrown when Composable throws an exception.
+ }
+ }
+}
+
+class MyTestComposables {
+
+ @Composable
+ fun MyWorkingComposable() {}
+
+ @Composable
+ fun MyThrowExceptionComposable() {
+ throw Exception("An Exception")
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
index c73edb2..adea0bf 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/ComposeViewAdapterTest.kt
@@ -20,6 +20,7 @@
import android.os.Build
import android.os.Bundle
import androidx.compose.animation.core.InternalAnimationApi
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
import androidx.compose.ui.tooling.animation.PreviewAnimationClock
import androidx.compose.ui.tooling.animation.UnsupportedComposeAnimation
import androidx.compose.ui.tooling.data.UiToolingDataApi
@@ -173,11 +174,29 @@
fun animateXAsStateIsSubscribed() {
checkAnimationsAreSubscribed(
"AnimateAsStatePreview",
- listOf("DpAnimation", "IntAnimation")
+ animateXAsState = listOf("DpAnimation", "IntAnimation")
)
}
@Test
+ fun animateXAsStateIsNotSubscribed() {
+ AnimateXAsStateComposeAnimation.testOverrideAvailability(false)
+ checkAnimationsAreSubscribed(
+ "AllAnimations",
+ unsupported = listOf(
+ "AnimatedContent",
+ "animateContentSize",
+ "TargetBasedAnimation",
+ "DecayAnimation",
+ "InfiniteTransition"
+ ),
+ transitions = listOf("checkBoxAnim", "Crossfade"),
+ animateXAsState = emptyList()
+ )
+ AnimateXAsStateComposeAnimation.testOverrideAvailability(true)
+ }
+
+ @Test
fun animateContentSizeIsSubscribed() {
checkAnimationsAreSubscribed("AnimateContentSizePreview", listOf("animateContentSize"))
}
@@ -208,7 +227,8 @@
checkAnimationsAreSubscribed(
"AllAnimations",
emptyList(),
- listOf("checkBoxAnim", "Crossfade")
+ listOf("checkBoxAnim", "Crossfade"),
+ animateXAsState = listOf("DpAnimation", "IntAnimation")
)
UnsupportedComposeAnimation.testOverrideAvailability(true)
}
@@ -224,8 +244,9 @@
private fun checkAnimationsAreSubscribed(
preview: String,
- unsupported: List<String>,
- transitions: List<String> = emptyList()
+ unsupported: List<String> = emptyList(),
+ transitions: List<String> = emptyList(),
+ animateXAsState: List<String> = emptyList()
) {
val clock = PreviewAnimationClock()
@@ -251,6 +272,8 @@
activityTestRule.runOnUiThread {
assertEquals(unsupported, clock.trackedUnsupportedAnimations.map { it.label })
assertEquals(transitions, clock.transitionClocks.values.map { it.animation.label })
+ assertEquals(animateXAsState,
+ clock.animateXAsStateClocks.values.map { it.animation.label })
assertEquals(0, clock.animatedVisibilityClocks.size)
}
}
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
index 1c3d41b..71c9d1d 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/TestAnimationPreview.kt
@@ -220,6 +220,36 @@
@Preview
@Composable
+fun AnimateAsStateWithLabelsPreview() {
+ var showMenu by remember { mutableStateOf(true) }
+ var message by remember { mutableStateOf("Hello") }
+
+ val size: Dp by animateDpAsState(
+ targetValue = if (showMenu) 0.dp else 10.dp,
+ animationSpec = spring(Spring.DampingRatioHighBouncy, Spring.StiffnessHigh),
+ label = "CustomDpLabel"
+ )
+ val offset by animateIntAsState(
+ targetValue = if (showMenu) 2 else 1,
+ label = "CustomIntLabel"
+ )
+
+ Box(
+ Modifier
+ .padding(size)
+ .offset(offset.dp)
+ .pointerInput(Unit) {
+ detectTapGestures {
+ showMenu = !showMenu
+ message += "!"
+ }
+ }) {
+ Text(text = message)
+ }
+}
+
+@Preview
+@Composable
fun CrossFadePreview() {
var currentPage by remember { mutableStateOf("A") }
Row {
@@ -243,6 +273,29 @@
@Preview
@Composable
+fun CrossFadeWithLabelPreview() {
+ var currentPage by remember { mutableStateOf("A") }
+ Row {
+ Button(>
+ currentPage = when (currentPage) {
+ "A" -> "B"
+ "B" -> "A"
+ else -> "A"
+ }
+ }) {
+ Text("Switch Page")
+ }
+ Crossfade(targetState = currentPage, label = "CrossfadeWithLabel") { screen ->
+ when (screen) {
+ "A" -> Text("Page A")
+ "B" -> Text("Page B")
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
fun AnimateContentSizePreview() {
var message by remember { mutableStateOf("Hello") }
Row {
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
new file mode 100644
index 0000000..459a6aa
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimationTest.kt
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.animateIntOffsetAsState
+import androidx.compose.animation.core.animateIntSizeAsState
+import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.animateRectAsState
+import androidx.compose.animation.core.animateSizeAsState
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.runtime.State
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AnimateXAsStateComposeAnimationTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun dpAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var dpState: State<Dp>? = null
+ rule.searchForAnimation(search) {
+ dpState = animateDpAsState(targetValue = 10.dp)
+ }
+ val composeAnimation = checkDefaultState(search, "DpAnimation", 10.dp)
+ rule.runOnUiThread {
+ composeAnimation.setState(20.dp)
+ }
+ rule.waitForIdle()
+ assertEquals(20.dp, dpState!!.value)
+ }
+
+ @Test
+ fun floatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Float>? = null
+ rule.searchForAnimation(search) {
+ state = animateFloatAsState(targetValue = 10f)
+ }
+ val composeAnimation = checkDefaultState(search, "FloatAnimation", 10f)
+ rule.runOnUiThread {
+ composeAnimation.setState(20f)
+ }
+ rule.waitForIdle()
+ assertEquals(20f, state!!.value)
+ }
+
+ @Test
+ fun intAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Int>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntAsState(targetValue = 10)
+ }
+ val composeAnimation = checkDefaultState(search, "IntAnimation", 10)
+ rule.runOnUiThread {
+ composeAnimation.setState(20)
+ }
+ rule.waitForIdle()
+ assertEquals(20, state!!.value)
+ }
+
+ @Test
+ fun intSizeAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<IntSize>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntSizeAsState(targetValue = IntSize(10, 20))
+ }
+ checkDefaultState(search, "IntSizeAnimation", IntSize(10, 20))
+ val composeAnimation = search.animations.first().parse()!!
+ rule.runOnUiThread {
+ composeAnimation.setState(IntSize(30, 40))
+ }
+ rule.waitForIdle()
+ assertEquals(IntSize(30, 40), state!!.value)
+ }
+
+ @Test
+ fun intOffsetAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<IntOffset>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntOffsetAsState(targetValue = IntOffset(10, 20))
+ }
+ val composeAnimation = checkDefaultState(search, "IntOffsetAnimation", IntOffset(10, 20))
+ rule.runOnUiThread {
+ composeAnimation.setState(IntOffset(30, 40))
+ }
+ rule.waitForIdle()
+ assertEquals(IntOffset(30, 40), state!!.value)
+ }
+
+ @Test
+ fun offsetAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Offset>? = null
+ rule.searchForAnimation(search) {
+ state = animateOffsetAsState(targetValue = Offset(10f, 20f))
+ }
+ val composeAnimation = checkDefaultState(search, "OffsetAnimation", Offset(10f, 20f))
+ rule.runOnUiThread {
+ composeAnimation.setState(Offset(30f, 40f))
+ }
+ rule.waitForIdle()
+ assertEquals(Offset(30f, 40f), state!!.value)
+ }
+
+ @Test
+ fun sizeAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Size>? = null
+ rule.searchForAnimation(search) {
+ state = animateSizeAsState(targetValue = Size(10f, 20f))
+ }
+ val composeAnimation = checkDefaultState(search, "SizeAnimation", Size(10f, 20f))
+ composeAnimation.setState(Size(30f, 40f))
+ rule.waitForIdle()
+ assertEquals(Size(30f, 40f), state!!.value)
+ }
+
+ @Test
+ fun rectAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Rect>? = null
+ rule.searchForAnimation(search) {
+ state = animateRectAsState(targetValue = Rect(10f, 20f, 30f, 40f))
+ }
+ val composeAnimation = checkDefaultState(search, "RectAnimation", Rect(10f, 20f, 30f, 40f))
+ composeAnimation.setState(Rect(50f, 60f, 70f, 80f))
+ rule.waitForIdle()
+ assertEquals(Rect(50f, 60f, 70f, 80f), state!!.value)
+ }
+
+ @Test
+ fun colorAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Color>? = null
+ rule.searchForAnimation(search) {
+ state = animateColorAsState(targetValue = Color(0.1f, 0.2f, 0.3f, 0.4f))
+ }
+ val composeAnimation =
+ checkDefaultState(search, "ColorAnimation", Color(0.1f, 0.2f, 0.3f, 0.4f))
+ composeAnimation.setState(Color(0.3f, 0.4f, 0.5f, 0.6f))
+ rule.waitForIdle()
+ assertEquals(Color(0.3f, 0.4f, 0.5f, 0.6f), state!!.value)
+ }
+
+ @Test
+ fun customFloatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch {}
+ var state: State<Float>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(10f, Float.VectorConverter)
+ }
+ val composeAnimation = checkDefaultState(search, "ValueAnimation", 10f)
+ composeAnimation.setState(30f)
+ rule.waitForIdle()
+ assertEquals(30f, state!!.value)
+ }
+
+ @Test
+ fun nullableFloatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Float?>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(1f, Utils.nullableFloatConverter)
+ }
+ val composeAnimation = checkDefaultState(search, "ValueAnimation", 1f)
+ composeAnimation.setState(30f)
+ rule.waitForIdle()
+ assertEquals(30f, state!!.value)
+ }
+
+ @Test
+ fun nullableFloatAnimationWithNullState() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ rule.searchForAnimation(search) {
+ animateValueAsState(null, Utils.nullableFloatConverter)
+ }
+ assertEquals(1, search.animations.size)
+ val composeAnimation = search.animations.first().parse()
+ assertNull(composeAnimation)
+ }
+
+ @Test
+ fun stringAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<String>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState("11.0", Utils.stringConverter)
+ }
+ val composeAnimation = checkDefaultState(search, "ValueAnimation", "11.0")
+ composeAnimation.setState("56.0")
+ rule.waitForIdle()
+ assertEquals("56.0", state!!.value)
+ }
+
+ @Test
+ fun enumAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Utils.EnumState>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(Utils.EnumState.One, Utils.enumConverter)
+ }
+ val composeAnimation = checkDefaultState(search, "ValueAnimation", Utils.EnumState.One, 3)
+ composeAnimation.setState(Utils.EnumState.Three)
+ rule.waitForIdle()
+ assertEquals(Utils.EnumState.Three, state!!.value)
+ }
+
+ @Test
+ fun nullableEnumAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Utils.EnumState?>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(Utils.EnumState.One, Utils.nullableEnumConverter)
+ }
+ val composeAnimation = checkDefaultState(search, "ValueAnimation", Utils.EnumState.One, 3)
+ composeAnimation.setState(Utils.EnumState.Three)
+ rule.waitForIdle()
+ assertEquals(Utils.EnumState.Three, state!!.value)
+ }
+
+ private fun checkDefaultState(
+ search: AnimationSearch.AnimateXAsStateSearch,
+ label: String,
+ defaultValue: Any,
+ numberOfStates: Int = 1
+ ): AnimateXAsStateComposeAnimation<*, *> {
+ assertEquals(1, search.animations.size)
+ val composeAnimation = search.animations.first().parse()!!
+ composeAnimation.animationObject.let {
+ assertNotNull(it)
+ assertEquals(defaultValue, it.value)
+ }
+ composeAnimation.animationSpec.let {
+ assertNotNull(it)
+ assertTrue(it is SpringSpec<*>)
+ }
+ assertEquals(label, composeAnimation.label)
+ assertEquals(numberOfStates, composeAnimation.states.size)
+ assertEquals(defaultValue, composeAnimation.states.first())
+ assertNotNull(composeAnimation.toolingState)
+ return composeAnimation
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
index bffb7df..2376088 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/AnimationSearchTest.kt
@@ -16,13 +16,16 @@
package androidx.compose.ui.tooling.animation
+import androidx.compose.animation.core.SpringSpec
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.tooling.AnimateAsStatePreview
+import androidx.compose.ui.tooling.AnimateAsStateWithLabelsPreview
import androidx.compose.ui.tooling.AnimateContentSizePreview
import androidx.compose.ui.tooling.AnimatedContentExtensionPreview
import androidx.compose.ui.tooling.AnimatedContentPreview
import androidx.compose.ui.tooling.AnimatedVisibilityPreview
import androidx.compose.ui.tooling.CrossFadePreview
+import androidx.compose.ui.tooling.CrossFadeWithLabelPreview
import androidx.compose.ui.tooling.DecayAnimationPreview
import androidx.compose.ui.tooling.InfiniteTransitionPreview
import androidx.compose.ui.tooling.TargetBasedAnimationPreview
@@ -32,6 +35,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
+import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -82,10 +86,46 @@
val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
rule.searchForAnimation(search) { AnimateAsStatePreview() }
assertEquals(2, search.animations.size)
+ search.animations.first().let {
+ Assert.assertTrue(it.animationSpec is SpringSpec)
+ Assert.assertNotNull(it.toolingState)
+ Assert.assertNotNull(it.animatable)
+ assertEquals("IntAnimation", it.animatable.label)
+ }
+ search.animations.last().let {
+ Assert.assertTrue(it.animationSpec is SpringSpec)
+ Assert.assertNotNull(it.toolingState)
+ Assert.assertNotNull(it.animatable)
+ assertEquals("DpAnimation", it.animatable.label)
+ }
search.track()
assertEquals(2, callbacks)
- assertEquals(0.dp, search.animations.last().targetValue)
- assertEquals(2, search.animations.first().targetValue)
+ assertEquals(0.dp, search.animations.last().animatable.targetValue)
+ assertEquals(2, search.animations.first().animatable.targetValue)
+ }
+
+ @Test
+ fun animatedXAsStateWithLabelsSearchIsFound() {
+ var callbacks = 0
+ val search = AnimationSearch.AnimateXAsStateSearch { callbacks++ }
+ rule.searchForAnimation(search) { AnimateAsStateWithLabelsPreview() }
+ assertEquals(2, search.animations.size)
+ search.animations.first().let {
+ Assert.assertTrue(it.animationSpec is SpringSpec)
+ Assert.assertNotNull(it.toolingState)
+ Assert.assertNotNull(it.animatable)
+ assertEquals("CustomIntLabel", it.animatable.label)
+ }
+ search.animations.last().let {
+ Assert.assertTrue(it.animationSpec is SpringSpec)
+ Assert.assertNotNull(it.toolingState)
+ Assert.assertNotNull(it.animatable)
+ assertEquals("CustomDpLabel", it.animatable.label)
+ }
+ search.track()
+ assertEquals(2, callbacks)
+ assertEquals(0.dp, search.animations.last().animatable.targetValue)
+ assertEquals(2, search.animations.first().animatable.targetValue)
}
@Test
@@ -137,6 +177,19 @@
search.track()
assertEquals(1, callbacks)
assertEquals("A", search.animations.first().targetState)
+ assertEquals("Crossfade", search.animations.first().label)
+ }
+
+ @Test
+ fun crossFadeWithLabelIsFoundAsTransition() {
+ var callbacks = 0
+ val search = AnimationSearch.TransitionSearch { callbacks++ }
+ rule.searchForAnimation(search) { CrossFadeWithLabelPreview() }
+ assertEquals(1, search.animations.size)
+ search.track()
+ assertEquals(1, callbacks)
+ assertEquals("A", search.animations.first().targetState)
+ assertEquals("CrossfadeWithLabel", search.animations.first().label)
}
@Test
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
index 52991f8..6015f60 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/Utils.kt
@@ -18,7 +18,10 @@
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.Transition
+import androidx.compose.animation.core.TwoWayConverter
import androidx.compose.animation.core.updateTransition
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -35,6 +38,40 @@
object Utils {
+ enum class EnumState { One, Two, Three }
+
+ val nullableFloatConverter = TwoWayConverter<Float?, AnimationVector1D>({
+ AnimationVector1D(it ?: 0f)
+ }, { it.value })
+
+ val stringConverter = TwoWayConverter<String, AnimationVector1D>(
+ { AnimationVector1D(it.toFloat()) }, { it.value.toString() })
+
+ val enumConverter = object : TwoWayConverter<EnumState, AnimationVector> {
+ override val convertFromVector: (AnimationVector) -> EnumState
+ get() = { EnumState.One }
+
+ override val convertToVector: (EnumState) -> AnimationVector
+ get() = { AnimationVector(1f) }
+ }
+
+ val nullableEnumConverter = object :
+ TwoWayConverter<EnumState?, AnimationVector> {
+ override val convertFromVector: (AnimationVector) -> EnumState?
+ get() = { EnumState.One }
+
+ override val convertToVector: (EnumState?) -> AnimationVector
+ get() = { AnimationVector(1f) }
+ }
+
+ val booleanConverter = object : TwoWayConverter<Boolean, AnimationVector1D> {
+ override val convertFromVector: (AnimationVector1D) -> Boolean
+ get() = { it.value == 1f }
+
+ override val convertToVector: (Boolean) -> AnimationVector1D
+ get() = { AnimationVector(if (it) 1f else 0f) }
+ }
+
@OptIn(UiToolingDataApi::class)
internal fun ComposeContentTestRule.searchForAnimation(
search: AnimationSearch.Search<*>,
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
new file mode 100644
index 0000000..e65ad4f
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
@@ -0,0 +1,674 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.animateIntAsState
+import androidx.compose.animation.core.animateIntOffsetAsState
+import androidx.compose.animation.core.animateIntSizeAsState
+import androidx.compose.animation.core.animateOffsetAsState
+import androidx.compose.animation.core.animateRectAsState
+import androidx.compose.animation.core.animateSizeAsState
+import androidx.compose.animation.core.animateValueAsState
+import androidx.compose.animation.tooling.ComposeAnimatedProperty
+import androidx.compose.animation.tooling.ComposeAnimation
+import androidx.compose.runtime.State
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.AnimationSearch
+import androidx.compose.ui.tooling.animation.Utils
+import androidx.compose.ui.tooling.animation.Utils.searchForAnimation
+import androidx.compose.ui.tooling.animation.states.ComposeAnimationState
+import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AnimateXAsStateClockTest {
+
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun dpAnimationClock() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Dp>? = null
+ rule.searchForAnimation(search) {
+ state = animateDpAsState(
+ targetValue = 10.dp, animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "DpAnimation",
+ initialValue = 10.dp,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(1.dp, 2.dp) }
+ checkUpdatedState(clock, label = "DpAnimation",
+ newInitialValue = 1.dp, newTargetValue = 2.dp,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3.dp), listOf(4.dp)) }
+ checkUpdatedState(clock, label = "DpAnimation",
+ newInitialValue = 3.dp, newTargetValue = 4.dp,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(111.dp, 111)
+ clock.setStateParameters(111.dp, null)
+ clock.setStateParameters(listOf(111.dp), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(111.dp), emptyList<Dp>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "DpAnimation",
+ newInitialValue = 3.dp, newTargetValue = 4.dp,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun floatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Float>? = null
+ rule.searchForAnimation(search) {
+ state = animateFloatAsState(
+ targetValue = 10f, animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "FloatAnimation",
+ initialValue = 10f,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+ checkUpdatedState(clock, label = "FloatAnimation",
+ newInitialValue = 1f, newTargetValue = 2f,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+ checkUpdatedState(clock, label = "FloatAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(111f, 111)
+ clock.setStateParameters(111f, null)
+ clock.setStateParameters(listOf(111f), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(111f), emptyList<Dp>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "FloatAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun intSizeAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<IntSize>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntSizeAsState(
+ targetValue = IntSize(10, 20),
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "IntSizeAnimation",
+ initialValue = IntSize(10, 20),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(IntSize(3, 4), IntSize(4, 5))
+ }
+ checkUpdatedState(clock, label = "IntSizeAnimation",
+ newInitialValue = IntSize(3, 4), newTargetValue = IntSize(4, 5),
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(5, 6), listOf(7, 8)) }
+ checkUpdatedState(clock, label = "IntSizeAnimation",
+ newInitialValue = IntSize(5, 6), newTargetValue = IntSize(7, 8),
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(IntSize(111, 111), 111)
+ clock.setStateParameters(IntSize(111, 111), null)
+ clock.setStateParameters(10, 10)
+ clock.setStateParameters(listOf(IntSize(111, 111)), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(IntSize(111, 11)), emptyList<IntOffset>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "IntSizeAnimation",
+ newInitialValue = IntSize(5, 6), newTargetValue = IntSize(7, 8),
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun intAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Int>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntAsState(
+ targetValue = 10, animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "IntAnimation",
+ initialValue = 10,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(1, 2) }
+ checkUpdatedState(clock, label = "IntAnimation",
+ newInitialValue = 1, newTargetValue = 2,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3), listOf(4)) }
+ checkUpdatedState(clock, label = "IntAnimation",
+ newInitialValue = 3, newTargetValue = 4,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(111, 111f)
+ clock.setStateParameters(111, null)
+ clock.setStateParameters(111f, 111f)
+ clock.setStateParameters(listOf(111), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(111), emptyList<Int>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "IntAnimation",
+ newInitialValue = 3, newTargetValue = 4,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun intOffsetAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<IntOffset>? = null
+ rule.searchForAnimation(search) {
+ state = animateIntOffsetAsState(
+ targetValue = IntOffset(10, 20),
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "IntOffsetAnimation",
+ initialValue = IntOffset(10, 20),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(IntOffset(1, 2), IntOffset(3, 4))
+ }
+ checkUpdatedState(clock, label = "IntOffsetAnimation",
+ newInitialValue = IntOffset(1, 2), newTargetValue = IntOffset(3, 4),
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3, 4), listOf(4, 5)) }
+ checkUpdatedState(clock, label = "IntOffsetAnimation",
+ newInitialValue = IntOffset(3, 4), newTargetValue = IntOffset(4, 5),
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(IntOffset(111, 111), 111)
+ clock.setStateParameters(IntOffset(111, 111), null)
+ clock.setStateParameters(10, 10)
+ clock.setStateParameters(listOf(IntOffset(111, 111)), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(IntOffset(111, 11)), emptyList<IntOffset>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "IntOffsetAnimation",
+ newInitialValue = IntOffset(3, 4), newTargetValue = IntOffset(4, 5),
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun offsetAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Offset>? = null
+ rule.searchForAnimation(search) {
+ state = animateOffsetAsState(
+ targetValue = Offset(10f, 20f),
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "OffsetAnimation",
+ initialValue = Offset(10f, 20f),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(Offset(1f, 2f), Offset(3f, 4f))
+ }
+ checkUpdatedState(clock, label = "OffsetAnimation",
+ newInitialValue = Offset(1f, 2f), newTargetValue = Offset(3f, 4f),
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3f, 4f), listOf(4f, 5f)) }
+ checkUpdatedState(clock, label = "OffsetAnimation",
+ newInitialValue = Offset(3f, 4f), newTargetValue = Offset(4f, 5f),
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(Offset(111f, 111f), 111)
+ clock.setStateParameters(Offset(111f, 111f), null)
+ clock.setStateParameters(10, 10)
+ clock.setStateParameters(listOf(Offset(111f, 111f)), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(Offset(111f, 111f)), emptyList<Offset>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "OffsetAnimation",
+ newInitialValue = Offset(3f, 4f), newTargetValue = Offset(4f, 5f),
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun sizeAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Size>? = null
+ rule.searchForAnimation(search) {
+ state = animateSizeAsState(
+ targetValue = Size(10f, 20f),
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "SizeAnimation",
+ initialValue = Size(10f, 20f),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(Size(1f, 2f), Size(3f, 4f))
+ }
+ checkUpdatedState(clock, label = "SizeAnimation",
+ newInitialValue = Size(1f, 2f), newTargetValue = Size(3f, 4f),
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3f, 4f), listOf(4f, 5f)) }
+ checkUpdatedState(clock, label = "SizeAnimation",
+ newInitialValue = Size(3f, 4f), newTargetValue = Size(4f, 5f),
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(Size(111f, 111f), 111)
+ clock.setStateParameters(Size(111f, 111f), null)
+ clock.setStateParameters(10, 10)
+ clock.setStateParameters(listOf(Size(111f, 111f)), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(Size(111f, 111f)), emptyList<Size>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "SizeAnimation",
+ newInitialValue = Size(3f, 4f), newTargetValue = Size(4f, 5f),
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun rectAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Rect>? = null
+ rule.searchForAnimation(search) {
+ state = animateRectAsState(
+ targetValue = Rect(10f, 20f, 30f, 40f),
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "RectAnimation",
+ initialValue = Rect(10f, 20f, 30f, 40f),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(
+ Rect(1f, 2f, 30f, 40f),
+ Rect(3f, 4f, 30f, 40f)
+ )
+ }
+ checkUpdatedState(clock, label = "RectAnimation",
+ newInitialValue = Rect(1f, 2f, 30f, 40f),
+ newTargetValue = Rect(3f, 4f, 30f, 40f),
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(
+ listOf(3f, 4f, 30f, 40f),
+ listOf(4f, 5f, 30f, 40f)
+ )
+ }
+ checkUpdatedState(clock, label = "RectAnimation",
+ newInitialValue = Rect(3f, 4f, 30f, 40f),
+ newTargetValue = Rect(4f, 5f, 30f, 40f),
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(Rect(42f, 42f, 42f, 42f), 42f)
+ clock.setStateParameters(Rect(42f, 42f, 42f, 42f), null)
+ clock.setStateParameters(42f, 42f)
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(Rect(42f, 42f, 42f, 42f)), listOf(42f))
+ clock.setStateParameters(listOf(Rect(42f, 42f, 42f, 42f)), emptyList<Rect>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "RectAnimation",
+ newInitialValue = Rect(3f, 4f, 30f, 40f),
+ newTargetValue = Rect(4f, 5f, 30f, 40f),
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun colorAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Color>? = null
+ rule.searchForAnimation(search) {
+ state = animateColorAsState(
+ targetValue = Color.Black,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ColorAnimation",
+ initialValue = Color.Black,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(Color.Gray, Color.Red) }
+ checkUpdatedState(clock, label = "ColorAnimation",
+ newInitialValue = Color.Gray, newTargetValue = Color.Red,
+ composeState = { state!!.value })
+ rule.runOnUiThread {
+ clock.setStateParameters(
+ listOf(Color.Blue.red, Color.Blue.green, Color.Blue.blue, Color.Blue.alpha),
+ listOf(Color.Yellow.red, Color.Yellow.green, Color.Yellow.blue, Color.Yellow.alpha)
+ )
+ }
+ checkUpdatedState(clock, label = "ColorAnimation",
+ newInitialValue = Color.Blue, newTargetValue = Color.Yellow,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(Color.Red, 1)
+ clock.setStateParameters(Color.Red, null)
+ clock.setStateParameters(10, 10)
+ clock.setStateParameters(listOf(Color.Red), listOf(0.1f, 0.2f, 0.3f))
+ clock.setStateParameters(listOf(Color.Red), emptyList<Color>())
+ clock.setStateParameters(listOf(null), listOf(null))
+ // Invalid arguments for color.
+ clock.setStateParameters(listOf(10f, 10f, 10f, 10f), listOf(10f, 10f, 10f, 10f))
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "ColorAnimation",
+ newInitialValue = Color.Blue, newTargetValue = Color.Yellow,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun customFloatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Float>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = 10f,
+ Float.VectorConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = 10f,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 1f, newTargetValue = 2f,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(30f, 40)
+ clock.setStateParameters(30f, null)
+ clock.setStateParameters(30L, 40L)
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(30f), listOf(40L))
+ clock.setStateParameters(emptyList<Float>(), emptyList<Float>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun nullableFloatAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Float?>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = 10f,
+ Utils.nullableFloatConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = 10f,
+ composeState = { state!!.value!! })
+ rule.runOnUiThread { clock.setStateParameters(1f, 2f) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 1f, newTargetValue = 2f,
+ composeState = { state!!.value!! })
+ rule.runOnUiThread { clock.setStateParameters(listOf(3f), listOf(4f)) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value!! })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(30f, 40)
+ clock.setStateParameters(30f, null)
+ clock.setStateParameters(30L, 40L)
+ clock.setStateParameters(listOf(30f), listOf(40L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(emptyList<Float>(), emptyList<Float>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = 3f, newTargetValue = 4f,
+ composeState = { state!!.value!! })
+ }
+
+ @Test
+ fun stringAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<String>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = "10.0",
+ Utils.stringConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = "10.0",
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters("20.0", "30.0") }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = "20.0", newTargetValue = "30.0",
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf("40.0"), listOf("50.0")) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = "40.0", newTargetValue = "50.0",
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(30f, 40)
+ clock.setStateParameters(30f, null)
+ clock.setStateParameters(30f, 40f)
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(30f), listOf(40L))
+ clock.setStateParameters(emptyList<String>(), emptyList<Int>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = "40.0", newTargetValue = "50.0",
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun enumAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Utils.EnumState>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = Utils.EnumState.One,
+ Utils.enumConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = Utils.EnumState.One,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun booleanAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Boolean>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = false,
+ Utils.booleanConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = false,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(false, true) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = false, newTargetValue = true,
+ composeState = { state!!.value })
+ rule.runOnUiThread { clock.setStateParameters(listOf(true), listOf(false)) }
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = true, newTargetValue = false,
+ composeState = { state!!.value })
+ // Invalid parameters are ignored.
+ rule.runOnUiThread {
+ clock.setStateParameters(true, 111f)
+ clock.setStateParameters(true, null)
+ clock.setStateParameters(42f, 42f)
+ clock.setStateParameters(listOf(true), listOf(111L))
+ clock.setStateParameters(listOf(null), listOf(null))
+ clock.setStateParameters(listOf(true), emptyList<Boolean>())
+ }
+ // State hasn't changed.
+ checkUpdatedState(clock, label = "ValueAnimation",
+ newInitialValue = true, newTargetValue = false,
+ composeState = { state!!.value })
+ }
+
+ @Test
+ fun nullableEnumAnimation() {
+ val search = AnimationSearch.AnimateXAsStateSearch { }
+ var state: State<Utils.EnumState?>? = null
+ rule.searchForAnimation(search) {
+ state = animateValueAsState(
+ targetValue = Utils.EnumState.One,
+ Utils.nullableEnumConverter,
+ animationSpec = TweenSpec(durationMillis = 100)
+ )
+ }
+ val clock = AnimateXAsStateClock(search.animations.first().parse()!!)
+ checkInitialState(clock, label = "ValueAnimation",
+ initialValue = Utils.EnumState.One,
+ composeState = { state!!.value!! })
+ }
+
+ private fun <T : ComposeAnimation, TState : ComposeAnimationState, V : Any>
+ checkInitialState(
+ clock: ComposeAnimationClock<T, TState>,
+ label: String,
+ initialValue: V,
+ composeState: () -> V
+ ) {
+ // Check default state.
+ assertEquals(initialValue, composeState())
+ assertEquals(TargetState(initialValue, initialValue), clock.state)
+ assertEquals(100L, clock.getMaxDuration())
+ assertEquals(100L, clock.getMaxDurationPerIteration())
+ assertEquals(
+ listOf(ComposeAnimatedProperty(label, initialValue)),
+ clock.getAnimatedProperties()
+ )
+ val transitions = clock.getTransitions(100)
+ assertEquals(1, transitions.size)
+ transitions.first().let {
+ assertEquals(label, it.label)
+ assertEquals(0L, it.startTimeMillis)
+ assertEquals(100L, it.endTimeMillis)
+ assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
+ assertEquals(mapOf(0L to initialValue, 100L to initialValue), it.values)
+ }
+ }
+
+ private fun <T : ComposeAnimation, TState : ComposeAnimationState, V : Any>
+ checkUpdatedState(
+ clock: ComposeAnimationClock<T, TState>,
+ label: String,
+ newInitialValue: V,
+ newTargetValue: V,
+ composeState: () -> V
+ ) {
+ rule.waitForIdle()
+ // Check new state.
+ rule.runOnUiThread {
+ clock.setClockTime(0)
+ assertEquals(newInitialValue, composeState())
+ assertEquals(TargetState(newInitialValue, newTargetValue), clock.state)
+ assertEquals(100L, clock.getMaxDuration())
+ assertEquals(100L, clock.getMaxDurationPerIteration())
+ assertEquals(
+ listOf(ComposeAnimatedProperty(label, newInitialValue)),
+ clock.getAnimatedProperties()
+ )
+ val newTransitions = clock.getTransitions(100)
+ assertEquals(1, newTransitions.size)
+ newTransitions.first().let {
+ assertEquals(label, it.label)
+ assertEquals(0L, it.startTimeMillis)
+ assertEquals(100L, it.endTimeMillis)
+ assertEquals("androidx.compose.animation.core.TweenSpec", it.specType)
+ assertEquals(mapOf(0L to newInitialValue, 100L to newTargetValue), it.values)
+ }
+ }
+ // Jump to the end of the animation.
+ rule.runOnUiThread {
+ clock.setClockTime(millisToNanos(300L))
+ }
+ // Check state at the end of the animation.
+ rule.waitForIdle()
+ rule.runOnUiThread {
+ assertEquals(newTargetValue, composeState())
+ val properties = clock.getAnimatedProperties()
+ assertEquals(listOf(ComposeAnimatedProperty(label, newTargetValue)), properties)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
index 14b8d3d..4f473ff 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/ComposeViewAdapter.kt
@@ -48,8 +48,9 @@
import androidx.compose.ui.platform.LocalFontLoader
import androidx.compose.ui.platform.ViewRootForTest
import androidx.compose.ui.text.font.createFontFamilyResolver
-import androidx.compose.ui.tooling.animation.PreviewAnimationClock
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
import androidx.compose.ui.tooling.animation.AnimationSearch
+import androidx.compose.ui.tooling.animation.PreviewAnimationClock
import androidx.compose.ui.tooling.animation.UnsupportedComposeAnimation
import androidx.compose.ui.tooling.data.Group
import androidx.compose.ui.tooling.data.SourceLocation
@@ -314,23 +315,29 @@
clock.trackAnimatedVisibility(it, ::requestLayout)
}
// All supported animations.
- val supportedSearch = setOf(
+ fun supportedSearch() = setOf(
transitionSearch,
animatedVisibilitySearch,
)
- // All unsupported animations, if API is available.
- val extraSearch = if (UnsupportedComposeAnimation.apiAvailable) setOf(
+ fun unsupportedSearch() = if (UnsupportedComposeAnimation.apiAvailable) setOf(
animatedContentSearch,
- AnimationSearch.AnimateXAsStateSearch { clock.trackAnimateXAsState(it) },
AnimationSearch.AnimateContentSizeSearch { clock.trackAnimateContentSize(it) },
AnimationSearch.TargetBasedSearch { clock.trackTargetBasedAnimations(it) },
AnimationSearch.DecaySearch { clock.trackDecayAnimations(it) },
AnimationSearch.InfiniteTransitionSearch { clock.trackInfiniteTransition(it) }
) else emptyList()
+ fun animateXAsStateSearch() =
+ if (AnimateXAsStateComposeAnimation.apiAvailable)
+ setOf(AnimationSearch.AnimateXAsStateSearch { clock.trackAnimateXAsState(it) })
+ else emptyList()
+
+ // All unsupported animations, if API is available.
+ val extraSearch = unsupportedSearch() + animateXAsStateSearch()
+
// Animations to track in PreviewAnimationClock.
- val setToTrack = supportedSearch + extraSearch
+ val setToTrack = supportedSearch() + extraSearch
// Animations to search. animatedContentSearch is included even if it's not going to be
// tracked as it should be excluded from transitionSearch.
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt
new file mode 100644
index 0000000..981a832
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.android.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+import android.util.Log
+
+private const val Tag = "PreviewLogger"
+
+internal actual class PreviewLogger {
+
+ actual companion object {
+ internal actual fun logWarning(message: String, throwable: Throwable?) {
+ Log.w(Tag, message, throwable)
+ }
+
+ internal actual fun logError(message: String, throwable: Throwable?) {
+ Log.e(Tag, message, throwable)
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
index 43a9ec6..6398dee 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/PreviewUtils.kt
@@ -16,7 +16,6 @@
package androidx.compose.ui.tooling
-import android.util.Log
import androidx.compose.ui.tooling.data.Group
import androidx.compose.ui.tooling.data.UiToolingDataApi
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
@@ -29,7 +28,7 @@
@Suppress("UNCHECKED_CAST")
return Class.forName(this) as? Class<out PreviewParameterProvider<*>>
} catch (e: ClassNotFoundException) {
- Log.e("PreviewProvider", "Unable to find provider '$this'", e)
+ PreviewLogger.logError("Unable to find PreviewProvider '$this'", e)
return null
}
}
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt
new file mode 100644
index 0000000..a1e339b
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimateXAsStateComposeAnimation.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.tooling.ComposeAnimation
+import androidx.compose.animation.tooling.ComposeAnimationType
+import org.jetbrains.annotations.TestOnly
+
+/**
+ * [ComposeAnimation] of type [ComposeAnimationType.ANIMATE_X_AS_STATE].
+ */
+internal class AnimateXAsStateComposeAnimation<T, V : AnimationVector>
+private constructor(
+ val toolingState: ToolingState<T>,
+ val animationSpec: AnimationSpec<T>,
+ override val animationObject: Animatable<T, V>,
+) : ComposeAnimation {
+ override val type = ComposeAnimationType.ANIMATE_X_AS_STATE
+
+ override val states: Set<Any> = (animationObject.value as Any).let {
+ it.javaClass.enumConstants?.toSet() ?: setOf(it)
+ }
+
+ override val label: String = animationObject.label
+
+ @Suppress("UNCHECKED_CAST")
+ fun setState(value: Any) {
+ toolingState.value = value as T
+ }
+
+ companion object {
+
+ /**
+ * [ComposeAnimationType] from ANIMATABLE to UNSUPPORTED are not available in previous
+ * versions of the library. To avoid creating non-existing enum,
+ * [UnsupportedComposeAnimation] should only be instantiated if [ComposeAnimationType] API
+ * for UNSUPPORTED enum is available.
+ */
+ var apiAvailable = enumValues<ComposeAnimationType>().any { it.name == "UNSUPPORTED" }
+ private set
+
+ internal fun <T, V : AnimationVector> AnimationSearch
+ .AnimateXAsStateSearchInfo<T, V>.parse():
+ AnimateXAsStateComposeAnimation<*, *>? {
+ if (!apiAvailable) return null
+ // Tooling can't control nullable Animatable with value set to null.
+ if (animatable.value == null) return null
+ return AnimateXAsStateComposeAnimation(
+ toolingState, animationSpec, animatable
+ )
+ }
+
+ /** This method is for testing only. */
+ @TestOnly
+ fun testOverrideAvailability(override: Boolean) {
+ apiAvailable = override
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
index f439810..790ecc0 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
@@ -17,10 +17,15 @@
package androidx.compose.ui.tooling.animation
import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationVector
import androidx.compose.animation.core.DecayAnimation
import androidx.compose.animation.core.InfiniteTransition
import androidx.compose.animation.core.TargetBasedAnimation
import androidx.compose.animation.core.Transition
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.ui.tooling.data.CallGroup
import androidx.compose.ui.tooling.data.Group
import androidx.compose.ui.tooling.data.UiToolingDataApi
import androidx.compose.ui.tooling.firstOrNull
@@ -32,6 +37,7 @@
private const val ANIMATED_VISIBILITY = "AnimatedVisibility"
private const val ANIMATE_VALUE_AS_STATE = "animateValueAsState"
private const val REMEMBER = "remember"
+private const val REMEMBER_UPDATED_STATE = "rememberUpdatedState"
private const val SIZE_ANIMATION_MODIFIER = "androidx.compose.animation.SizeAnimationModifier"
/** Find first data with type [T] within all remember calls. */
@@ -93,10 +99,21 @@
class InfiniteTransitionSearch(trackAnimation: (InfiniteTransition) -> Unit) :
RememberSearch<InfiniteTransition>(InfiniteTransition::class, trackAnimation)
+ data class AnimateXAsStateSearchInfo<T, V : AnimationVector>(
+ val animatable: Animatable<T, V>,
+ val animationSpec: AnimationSpec<T>,
+ val toolingState: ToolingState<T>
+ )
+
/** Search for animateXAsState() and animateValueAsState() animations. */
- class AnimateXAsStateSearch(trackAnimation: (Animatable<*, *>) -> Unit) :
- Search<Animatable<*, *>>(trackAnimation) {
+ class AnimateXAsStateSearch(trackAnimation: (AnimateXAsStateSearchInfo<*, *>) -> Unit) :
+ Search<AnimateXAsStateSearchInfo<*, *>>(trackAnimation) {
override fun addAnimations(groupsWithLocation: Collection<Group>) {
+ animations.addAll(findAnimations<Any?>(groupsWithLocation))
+ }
+
+ private fun <T> findAnimations(groupsWithLocation: Collection<Group>):
+ List<AnimateXAsStateSearchInfo<T, AnimationVector>> {
// How "animateXAsState" calls organized:
// Group with name "animateXAsState", for example animateDpAsState, animateIntAsState
// children
@@ -107,12 +124,37 @@
// To distinguish Animatable within "animateXAsState" calls from other Animatables,
// first "animateValueAsState" calls are found.
// Find Animatable within "animateValueAsState" call.
- animations.addAll(
- groupsWithLocation.filter { call -> call.name == ANIMATE_VALUE_AS_STATE }
- .mapNotNull { animateValue ->
- animateValue.children.findRememberedData<Animatable<*, *>>().firstOrNull()
- }.toSet()
- )
+ val groups = groupsWithLocation.filter { group -> group.name == ANIMATE_VALUE_AS_STATE }
+ .filterIsInstance<CallGroup>()
+ return groups.mapNotNull {
+ val animatable = findAnimatable<T>(it)
+ val spec = findAnimationSpec<T>(it)
+ val toolingOverride =
+ it.children.findRememberedData<MutableState<State<T>?>>().firstOrNull()
+ if (animatable != null && spec != null && toolingOverride != null) {
+ if (toolingOverride.value == null) {
+ toolingOverride.value = ToolingState(animatable.value)
+ }
+ AnimateXAsStateSearchInfo(
+ animatable,
+ spec,
+ toolingOverride.value as ToolingState<T>
+ )
+ } else null
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun <T> findAnimationSpec(group: CallGroup): AnimationSpec<T>? {
+ return group.children.filter { it.name == REMEMBER_UPDATED_STATE }
+ .flatMap { it.data }
+ .filterIsInstance<State<T>>().map { it.value }
+ .filterIsInstance<AnimationSpec<T>>().firstOrNull()
+ }
+
+ private fun <T> findAnimatable(group: CallGroup): Animatable<T, AnimationVector>? {
+ return group.children.findRememberedData<Animatable<T, AnimationVector>>()
+ .firstOrNull()
}
}
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
index a24f04c..e065db8 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
@@ -18,7 +18,6 @@
import android.util.Log
import androidx.annotation.VisibleForTesting
-import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.DecayAnimation
import androidx.compose.animation.core.InfiniteTransition
import androidx.compose.animation.core.TargetBasedAnimation
@@ -26,6 +25,8 @@
import androidx.compose.animation.tooling.ComposeAnimatedProperty
import androidx.compose.animation.tooling.ComposeAnimation
import androidx.compose.animation.tooling.TransitionInfo
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation.Companion.parse
+import androidx.compose.ui.tooling.animation.clock.AnimateXAsStateClock
import androidx.compose.ui.tooling.animation.clock.AnimatedVisibilityClock
import androidx.compose.ui.tooling.animation.clock.ComposeAnimationClock
import androidx.compose.ui.tooling.animation.clock.TransitionClock
@@ -72,12 +73,19 @@
internal val animatedVisibilityClocks =
mutableMapOf<AnimatedVisibilityComposeAnimation, AnimatedVisibilityClock>()
+ /** Map of subscribed [AnimateXAsStateComposeAnimation]s and corresponding [AnimateXAsStateClock]s. */
+ @VisibleForTesting
+ internal val animateXAsStateClocks =
+ mutableMapOf<AnimateXAsStateComposeAnimation<*, *>, AnimateXAsStateClock<*, *>>()
+
/** All subscribed animations clocks. */
private val allClocks: List<ComposeAnimationClock<*, *>>
- get() = transitionClocks.values + animatedVisibilityClocks.values
+ get() = transitionClocks.values +
+ animatedVisibilityClocks.values + animateXAsStateClocks.values
private fun findClock(animation: ComposeAnimation): ComposeAnimationClock<*, *>? {
return transitionClocks[animation] ?: animatedVisibilityClocks[animation]
+ ?: animateXAsStateClocks[animation]
}
fun trackTransition(animation: Transition<*>) {
@@ -106,8 +114,13 @@
}
}
- fun trackAnimateXAsState(animation: Animatable<*, *>) {
- trackUnsupported(animation, animation.label)
+ fun trackAnimateXAsState(animation: AnimationSearch.AnimateXAsStateSearchInfo<*, *>) {
+ trackAnimation(animation.animatable) {
+ animation.parse()?.let {
+ animateXAsStateClocks[it] = AnimateXAsStateClock(it)
+ notifySubscribe(it)
+ }
+ }
}
fun trackAnimateContentSize(animation: Any) {
@@ -185,7 +198,7 @@
* Expected to be called via reflection from Android Studio.
*/
fun updateFromAndToStates(composeAnimation: ComposeAnimation, fromState: Any, toState: Any) {
- transitionClocks[composeAnimation]?.setStateParameters(fromState, toState)
+ findClock(composeAnimation)?.setStateParameters(fromState, toState)
}
/**
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt
new file mode 100644
index 0000000..c785e65
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/ToolingState.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/**
+ * Tooling can override [mutableStateOf] in [Composable] with [ToolingState].
+ * [Composable] should declare a state, which could be private:
+ *
+ * val toolingOverride = remember { mutableStateOf<State<T>?>(null) }
+ *
+ * Tooling overrides `toolingOverride` with
+ *
+ * toolingOverride.value = ToolingState(default)
+ *
+ * @param default default value
+ */
+class ToolingState<T>(default: T) : State<T> {
+ override var value by mutableStateOf(default)
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
new file mode 100644
index 0000000..d80cf01
--- /dev/null
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling.animation.clock
+
+import androidx.compose.animation.core.AnimationVector
+import androidx.compose.animation.core.TargetBasedAnimation
+import androidx.compose.animation.tooling.ComposeAnimatedProperty
+import androidx.compose.animation.tooling.TransitionInfo
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
+import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+
+/**
+ * [ComposeAnimationClock] for [AnimateXAsStateComposeAnimation].
+ */
+internal class AnimateXAsStateClock<T, V : AnimationVector>(
+ override val animation: AnimateXAsStateComposeAnimation<T, V>
+) :
+ ComposeAnimationClock<AnimateXAsStateComposeAnimation<T, V>, TargetState<T>> {
+
+ override var state = TargetState(
+ animation.animationObject.value,
+ animation.animationObject.value
+ )
+ set(value) {
+ field = value
+ currAnimation = getCurrentAnimation()
+ setClockTime(0)
+ }
+
+ private var currentValue: T = animation.toolingState.value
+ private set(value) {
+ field = value
+ animation.toolingState.value = value
+ }
+
+ private var currAnimation: TargetBasedAnimation<T, V> = getCurrentAnimation()
+
+ @Suppress("UNCHECKED_CAST")
+ override fun setStateParameters(par1: Any, par2: Any?) {
+
+ fun parametersAreValid(par1: Any?, par2: Any?): Boolean {
+ return currentValue != null &&
+ par1 != null && par2 != null && par1::class == par2::class
+ }
+
+ fun parametersHasTheSameType(value: Any, par1: Any, par2: Any): Boolean {
+ return value::class == par1::class && value::class == par2::class
+ }
+
+ if (!parametersAreValid(par1, par2)) return
+
+ if (parametersHasTheSameType(currentValue!!, par1, par2!!)) {
+ state = TargetState(par1 as T, par2 as T)
+ return
+ }
+
+ if (par1 is List<*> && par2 is List<*>) {
+ try {
+ state = when (currentValue) {
+ is IntSize -> TargetState(
+ IntSize(par1[0] as Int, par1[1] as Int),
+ IntSize(par2[0] as Int, par2[1] as Int)
+ )
+
+ is IntOffset -> TargetState(
+ IntOffset(par1[0] as Int, par1[1] as Int),
+ IntOffset(par2[0] as Int, par2[1] as Int)
+ )
+
+ is Size -> TargetState(
+ Size(par1[0] as Float, par1[1] as Float),
+ Size(par2[0] as Float, par2[1] as Float)
+ )
+
+ is Offset -> TargetState(
+ Offset(par1[0] as Float, par1[1] as Float),
+ Offset(par2[0] as Float, par2[1] as Float)
+ )
+
+ is Rect ->
+ TargetState(
+ Rect(
+ par1[0] as Float,
+ par1[1] as Float,
+ par1[2] as Float,
+ par1[3] as Float
+ ),
+ Rect(
+ par2[0] as Float,
+ par2[1] as Float,
+ par2[2] as Float,
+ par2[3] as Float
+ ),
+ )
+ is Color -> TargetState(
+ Color(
+ par1[0] as Float,
+ par1[1] as Float,
+ par1[2] as Float,
+ par1[3] as Float
+ ),
+ Color(
+ par2[0] as Float,
+ par2[1] as Float,
+ par2[2] as Float,
+ par2[3] as Float
+ ),
+ )
+ else -> {
+ if (parametersAreValid(par1[0], par2[0]) &&
+ parametersHasTheSameType(currentValue!!, par1[0]!!, par2[0]!!)
+ ) TargetState(par1[0], par2[0])
+ else return
+ }
+ } as TargetState<T>
+ } catch (_: IndexOutOfBoundsException) {
+ return
+ } catch (_: ClassCastException) {
+ return
+ } catch (_: IllegalArgumentException) {
+ return
+ } catch (_: NullPointerException) {
+ return
+ }
+ }
+ }
+
+ override fun getAnimatedProperties(): List<ComposeAnimatedProperty> {
+ return listOf(ComposeAnimatedProperty(animation.label, currentValue as Any))
+ }
+
+ override fun getMaxDurationPerIteration(): Long {
+ return nanosToMillis(currAnimation.durationNanos)
+ }
+
+ override fun getMaxDuration(): Long {
+ return nanosToMillis(currAnimation.durationNanos)
+ }
+
+ override fun getTransitions(stepMillis: Long): List<TransitionInfo> {
+ return listOf(
+ currAnimation.createTransitionInfo(
+ animation.label, animation.animationSpec, stepMillis
+ )
+ )
+ }
+
+ private var clockTimeNanos = 0L
+ set(value) {
+ field = value
+ currentValue = currAnimation.getValueFromNanos(value)
+ }
+
+ override fun setClockTime(animationTimeNanos: Long) {
+ clockTimeNanos = animationTimeNanos
+ }
+
+ private fun getCurrentAnimation(): TargetBasedAnimation<T, V> {
+ return TargetBasedAnimation(
+ animationSpec = animation.animationSpec,
+ initialValue = state.initial,
+ targetValue = state.target,
+ typeConverter = animation.animationObject.typeConverter,
+ initialVelocity = animation.animationObject.velocity
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt
new file mode 100644
index 0000000..7b9ca9f
--- /dev/null
+++ b/compose/ui/ui-tooling/src/desktopMain/kotlin/androidx/compose/desktop/ui/tooling/PreviewLogger.desktop.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.tooling
+
+private const val Tag = "PreviewLogger"
+
+internal actual class PreviewLogger {
+
+ actual companion object {
+ internal actual fun logWarning(message: String, throwable: Throwable?) {
+ println("$Tag: $message.\n$throwable")
+ }
+
+ internal actual fun logError(message: String, throwable: Throwable?) {
+ System.err.println("$Tag: $message.\n$throwable")
+ }
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
index 28d41ec..04b7bfd 100644
--- a/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/ComposableInvoker.kt
@@ -200,7 +200,10 @@
method.invokeComposableMethod(instance, composer, *args)
}
} catch (e: ReflectiveOperationException) {
- throw ClassNotFoundException("Composable Method '$className.$methodName' not found", e)
+ PreviewLogger.logWarning(
+ "Failed to invoke Composable Method '$className.$methodName'"
+ )
+ throw e
}
}
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
similarity index 67%
rename from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
rename to compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
index 79122bf..c26ea17 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/compose/ui/ui-tooling/src/jvmMain/kotlin/androidx/compose/ui/tooling/PreviewLogger.jvm.kt
@@ -14,10 +14,13 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.compose.ui.tooling
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
+internal expect class PreviewLogger {
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+ companion object {
+ internal fun logWarning(message: String, throwable: Throwable? = null)
+
+ internal fun logError(message: String, throwable: Throwable? = null)
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 91506e6..7dd6735 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2425,6 +2425,7 @@
}
public final class DelegatableNodeKt {
+ method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateSubtree(androidx.compose.ui.node.DelegatableNode);
}
@androidx.compose.ui.ExperimentalComposeUiApi public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
index ad1c03f..8bed753 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawModifierTest.kt
@@ -22,6 +22,7 @@
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -31,6 +32,7 @@
import androidx.compose.testutils.assertPixels
import androidx.compose.ui.AtLeastSize
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
@@ -97,26 +99,29 @@
var rectColor by remember { mutableStateOf(Color.Blue) }
AtLeastSize(
size = size,
- modifier = Modifier.testTag(testTag).drawWithCache {
- val drawSize = this.size
- val path = Path().apply {
- lineTo(drawSize.width / 2f, 0f)
- lineTo(drawSize.width / 2f, drawSize.height)
- lineTo(0f, drawSize.height)
- close()
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ val drawSize = this.size
+ val path = Path().apply {
+ lineTo(drawSize.width / 2f, 0f)
+ lineTo(drawSize.width / 2f, drawSize.height)
+ lineTo(0f, drawSize.height)
+ close()
+ }
+ cacheBuildCount++
+ onDrawBehind {
+ drawRect(rectColor)
+ drawPath(path, Color.Red)
+ }
}
- cacheBuildCount++
- onDrawBehind {
- drawRect(rectColor)
- drawPath(path, Color.Red)
+ .clickable {
+ if (rectColor == Color.Blue) {
+ rectColor = Color.Green
+ } else {
+ rectColor = Color.Blue
+ }
}
- }.clickable {
- if (rectColor == Color.Blue) {
- rectColor = Color.Green
- } else {
- rectColor = Color.Blue
- }
- }
) { }
}
@@ -156,6 +161,34 @@
}
}
+ @Test
+ fun invalidationForDrawWithCache() {
+ var size by mutableStateOf(10f)
+ var drawCount = 0
+ rule.setContent {
+ Box(Modifier.fillMaxSize()) {
+ Box(Modifier
+ .graphicsLayer { }
+ .size(50.dp)
+ .drawWithCache {
+ val rectSize = Size(size, size)
+ onDrawBehind {
+ drawRect(Color.Blue, Offset.Zero, rectSize)
+ drawCount++
+ }
+ }
+ .graphicsLayer { }
+ )
+ }
+ }
+ rule.waitForIdle()
+ assertThat(drawCount).isEqualTo(1)
+
+ size = 15f
+ rule.waitForIdle()
+ assertThat(drawCount).isEqualTo(2)
+ }
+
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
@Test
fun testCacheInvalidatedAfterStateChange() {
@@ -169,22 +202,25 @@
var pathFillBounds by remember { mutableStateOf(false) }
AtLeastSize(
size = size,
- modifier = Modifier.testTag(testTag).drawWithCache {
- val pathSize = if (pathFillBounds) this.size else this.size / 2f
- val path = Path().apply {
- lineTo(pathSize.width, 0f)
- lineTo(pathSize.width, pathSize.height)
- lineTo(0f, pathSize.height)
- close()
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ val pathSize = if (pathFillBounds) this.size else this.size / 2f
+ val path = Path().apply {
+ lineTo(pathSize.width, 0f)
+ lineTo(pathSize.width, pathSize.height)
+ lineTo(0f, pathSize.height)
+ close()
+ }
+ cacheBuildCount++
+ onDrawBehind {
+ drawRect(Color.Red)
+ drawPath(path, Color.Blue)
+ }
}
- cacheBuildCount++
- onDrawBehind {
- drawRect(Color.Red)
- drawPath(path, Color.Blue)
+ .clickable {
+ pathFillBounds = !pathFillBounds
}
- }.clickable {
- pathFillBounds = !pathFillBounds
- }
) { }
}
@@ -269,25 +305,28 @@
var size by remember { mutableStateOf(startSize) }
AtLeastSize(
size = size,
- modifier = Modifier.testTag(testTag).drawWithCache {
- val drawSize = this.size
- val path = Path().apply {
- lineTo(drawSize.width, 0f)
- lineTo(drawSize.height, drawSize.height)
- lineTo(0f, drawSize.height)
- close()
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ val drawSize = this.size
+ val path = Path().apply {
+ lineTo(drawSize.width, 0f)
+ lineTo(drawSize.height, drawSize.height)
+ lineTo(0f, drawSize.height)
+ close()
+ }
+ cacheBuildCount++
+ onDrawBehind {
+ drawPath(path, Color.Red)
+ }
}
- cacheBuildCount++
- onDrawBehind {
- drawPath(path, Color.Red)
+ .clickable {
+ if (size == startSize) {
+ size = endSize
+ } else {
+ size = startSize
+ }
}
- }.clickable {
- if (size == startSize) {
- size = endSize
- } else {
- size = startSize
- }
- }
) { }
}
@@ -356,7 +395,9 @@
val color = remember { mutableStateOf(Color.Red) }
AtLeastSize(
size = startSize,
- modifier = Modifier.testTag(testTag).drawPathHelperModifier(color.value)
+ modifier = Modifier
+ .testTag(testTag)
+ .drawPathHelperModifier(color.value)
.clickable {
if (color.value == Color.Red) {
color.value = Color.Blue
@@ -403,7 +444,8 @@
Column {
AtLeastSize(
size = 50,
- modifier = Modifier.testTag(boxTag)
+ modifier = Modifier
+ .testTag(boxTag)
.graphicsLayer()
.drawWithCache {
// State read of flag
@@ -417,7 +459,8 @@
)
Box(
- Modifier.testTag(clickTag)
+ Modifier
+ .testTag(clickTag)
.size(20.dp)
.clickable {
flag.value = !flag.value
@@ -473,11 +516,14 @@
rule.setContent {
AtLeastSize(
size = testSize,
- modifier = Modifier.testTag(testTag).drawWithCache {
- onDrawBehind {
- drawRect(Color.Red, size = Size(size.width / 2, size.height))
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ onDrawBehind {
+ drawRect(Color.Red, size = Size(size.width / 2, size.height))
+ }
}
- }.background(Color.Blue)
+ .background(Color.Blue)
)
}
@@ -502,12 +548,15 @@
rule.setContent {
AtLeastSize(
size = testSize,
- modifier = Modifier.testTag(testTag).drawWithCache {
- onDrawWithContent {
- drawContent()
- drawRect(Color.Red, size = Size(size.width / 2, size.height))
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ onDrawWithContent {
+ drawContent()
+ drawRect(Color.Red, size = Size(size.width / 2, size.height))
+ }
}
- }.background(Color.Blue)
+ .background(Color.Blue)
)
}
@@ -536,12 +585,15 @@
rule.setContent {
AtLeastSize(
size = testSize,
- modifier = Modifier.testTag(testTag).drawWithCache {
- onDrawWithContent {
- drawContent()
- drawRect(Color.Green, blendMode = BlendMode.Plus)
+ modifier = Modifier
+ .testTag(testTag)
+ .drawWithCache {
+ onDrawWithContent {
+ drawContent()
+ drawRect(Color.Green, blendMode = BlendMode.Plus)
+ }
}
- }.background(Color.Blue)
+ .background(Color.Blue)
)
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
new file mode 100644
index 0000000..b7279e2
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/InvalidateSubtreeTest.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.compose.ui.node
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.DrawModifier
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.graphicsLayer
+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.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+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
+
+@OptIn(ExperimentalComposeUiApi::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class InvalidateSubtreeTest {
+ @get:Rule
+ val rule = createComposeRule()
+
+ @Test
+ fun invalidateSubtreeNoLayers() {
+ lateinit var invalidate: () -> Unit
+ val counter1 = LayoutAndDrawCounter()
+ val counter2 = LayoutAndDrawCounter()
+ val counter3 = LayoutAndDrawCounter()
+ val captureInvalidate = modifierElementOf(
+ create = {
+ val obj = object : Modifier.Node() {}
+ invalidate = { obj.invalidateSubtree() }
+ obj
+ }
+ )
+ rule.setContent {
+ Box(counter1) {
+ Box(counter2 then captureInvalidate) {
+ Box(counter3.size(10.dp))
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertThat(counter1.drawCount).isEqualTo(1)
+ assertThat(counter1.measureCount).isEqualTo(1)
+ assertThat(counter1.placeCount).isEqualTo(1)
+ assertThat(counter2.drawCount).isEqualTo(1)
+ assertThat(counter2.measureCount).isEqualTo(1)
+ assertThat(counter2.placeCount).isEqualTo(1)
+ assertThat(counter3.drawCount).isEqualTo(1)
+ assertThat(counter3.measureCount).isEqualTo(1)
+ assertThat(counter3.placeCount).isEqualTo(1)
+
+ rule.runOnUiThread {
+ invalidate()
+ }
+ rule.waitForIdle()
+
+ // There isn't a layer that can be invalidated, so we draw this twice also
+ assertThat(counter1.drawCount).isEqualTo(2)
+ assertThat(counter1.measureCount).isEqualTo(1)
+ assertThat(counter1.placeCount).isEqualTo(1)
+ assertThat(counter2.drawCount).isEqualTo(2)
+ assertThat(counter2.measureCount).isEqualTo(2)
+ assertThat(counter2.placeCount).isEqualTo(2)
+ assertThat(counter3.drawCount).isEqualTo(2)
+ assertThat(counter3.measureCount).isEqualTo(2)
+ assertThat(counter3.placeCount).isEqualTo(2)
+ }
+
+ @Test
+ fun invalidateSubtreeWithLayers() {
+ lateinit var invalidate: () -> Unit
+ val counter1 = LayoutAndDrawCounter()
+ val counter2 = LayoutAndDrawCounter()
+ val counter3 = LayoutAndDrawCounter()
+ val counter4 = LayoutAndDrawCounter()
+ val captureInvalidate = modifierElementOf(
+ create = {
+ val obj = object : Modifier.Node() {}
+ invalidate = { obj.invalidateSubtree() }
+ obj
+ }
+ )
+ rule.setContent {
+ Box(Modifier.graphicsLayer {} then counter1.graphicsLayer { }) {
+ Box(Modifier.graphicsLayer { } then
+ counter2 then captureInvalidate.graphicsLayer { } then counter3
+ ) {
+ Box(counter4.size(10.dp))
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertThat(counter1.drawCount).isEqualTo(1)
+ assertThat(counter2.drawCount).isEqualTo(1)
+ assertThat(counter3.drawCount).isEqualTo(1)
+ assertThat(counter4.drawCount).isEqualTo(1)
+
+ rule.runOnUiThread {
+ invalidate()
+ }
+ rule.waitForIdle()
+
+ assertThat(counter1.drawCount).isEqualTo(1)
+ assertThat(counter2.drawCount).isEqualTo(2)
+ assertThat(counter3.drawCount).isEqualTo(2)
+ assertThat(counter4.drawCount).isEqualTo(2)
+ }
+
+ private class LayoutAndDrawCounter : LayoutModifier, DrawModifier {
+ var measureCount = 0
+ var placeCount = 0
+ var drawCount = 0
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
+ measureCount++
+ val p = measurable.measure(constraints)
+ return layout(p.width, p.height) {
+ placeCount++
+ p.place(0, 0)
+ }
+ }
+
+ override fun ContentDrawScope.draw() {
+ drawCount++
+ drawContent()
+ }
+ }
+}
\ No newline at end of file
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 0c989fd..5575793 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
@@ -28,6 +28,7 @@
import androidx.core.view.doOnLayout
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
@@ -92,6 +93,7 @@
* [MonotonicFrameClock.withFrameNanos] will resume in time to make the current frame if called
* from the situation described above, and that subsequent calls will wait until the next frame.
*/
+ @FlakyTest(bugId = 255972660)
@Test
fun runsBeforeFrameDispatchedByInput() = runBlocking {
val ranInputJobOnFrame = CompletableDeferred<Int>()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 6fe7f05..da9ee0f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -70,6 +70,7 @@
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
import androidx.test.filters.MediumTest
import androidx.test.filters.SmallTest
import org.hamcrest.CoreMatchers.instanceOf
@@ -92,6 +93,7 @@
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
+ @FlakyTest(bugId = 256017578)
@Test
fun composeViewComposedContent() {
rule.activityRule.scenario.onActivity { activity ->
@@ -878,4 +880,4 @@
): Boolean {
return super.addViewInLayout(child, index, params, preventRequestLayout)
}
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 4f87447..b6a4e25 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -141,7 +141,7 @@
*
* The dialog is visible as long as it is part of the composition hierarchy.
* In order to let the user dismiss the Dialog, the implementation of [onDismissRequest] should
- * contain a way to remove to remove the dialog from the composition hierarchy.
+ * contain a way to remove the dialog from the composition hierarchy.
*
* Example usage:
*
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 9c135bd..215dd4d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -306,3 +306,17 @@
@ExperimentalComposeUiApi
internal fun DelegatableNode.requireOwner(): Owner = requireLayoutNode().owner!!
+
+/**
+ * Invalidates the subtree of this layout, including layout, drawing, parent data, etc.
+ *
+ * Calling this method can be a relatively expensive operation as it will cause the
+ * entire subtree to relayout and redraw instead of just parts that
+ * are otherwise invalidated. Its use should be limited to structural changes.
+ */
+@ExperimentalComposeUiApi
+fun DelegatableNode.invalidateSubtree() {
+ if (node.isAttached) {
+ requireLayoutNode().invalidateSubtree()
+ }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
index d123010..69c46bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
@@ -35,4 +35,8 @@
}
@ExperimentalComposeUiApi
-internal fun DrawModifierNode.requestDraw() = requireLayoutNode().invalidateLayer()
+internal fun DrawModifierNode.requestDraw() {
+ if (node.isAttached) {
+ requireCoordinator(Nodes.Any).invalidateLayer()
+ }
+}
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 a04cfee..85db8a6 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
@@ -1178,6 +1178,22 @@
*/
internal fun markLookaheadLayoutPending() = layoutDelegate.markLookaheadLayoutPending()
+ @OptIn(ExperimentalComposeUiApi::class)
+ fun invalidateSubtree(isRootOfInvalidation: Boolean = true) {
+ if (isRootOfInvalidation) {
+ parent?.invalidateLayer()
+ // Invalidate semantics. We can do this once because there isn't a node-by-node
+ // invalidation mechanism.
+ requireOwner().onSemanticsChange()
+ }
+ requestRemeasure()
+ nodes.headToTail(Nodes.Layout) {
+ it.requireCoordinator(Nodes.Layout).layer?.invalidate()
+ }
+ // TODO: invalidate parent data
+ _children.forEach { it.invalidateSubtree(false) }
+ }
+
/**
* Marks the layoutNode dirty for another lookahead measure pass.
*/
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index fc6a1b5f..9bd9dc8 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -21,9 +21,6 @@
@androidx.constraintlayout.compose.ExperimentalMotionApi public abstract class BaseKeyFrameScope {
method protected final <E extends androidx.constraintlayout.compose.NamedPropertyOrValue> kotlin.properties.ObservableProperty<E> addNameOnPropertyChange(E? initialValue, optional String? nameOverride);
method protected final <T> kotlin.properties.ObservableProperty<T> addOnPropertyChange(T? initialValue, optional String? nameOverride);
- method public final void addToContainer(androidx.constraintlayout.core.parser.CLContainer container);
- method protected final java.util.Map<java.lang.String,java.lang.Object> getUserAttributes();
- property protected final java.util.Map<java.lang.String,java.lang.Object> userAttributes;
}
@androidx.constraintlayout.compose.ExperimentalMotionApi public class BaseKeyFramesScope {
@@ -649,7 +646,15 @@
method public androidx.constraintlayout.compose.ConstraintSetRef constraintSet(optional String? name, optional androidx.constraintlayout.compose.ConstraintSetRef? extendConstraintSet, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.ConstraintSetScope,kotlin.Unit> constraintSetContent);
method public androidx.constraintlayout.compose.ConstrainedLayoutReference createRefFor(Object id);
method public void customColor(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customColor(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customDistance(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customDistance(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
method public void customFloat(androidx.constraintlayout.compose.ConstrainScope, String name, float value);
+ method public void customFloat(androidx.constraintlayout.compose.KeyAttributeScope, String name, float value);
+ method public void customFontSize(androidx.constraintlayout.compose.ConstrainScope, String name, long value);
+ method public void customFontSize(androidx.constraintlayout.compose.KeyAttributeScope, String name, long value);
+ method public void customInt(androidx.constraintlayout.compose.ConstrainScope, String name, int value);
+ method public void customInt(androidx.constraintlayout.compose.KeyAttributeScope, String name, int value);
method public void defaultTransition(androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, optional kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
method public void transition(optional String? name, androidx.constraintlayout.compose.ConstraintSetRef from, androidx.constraintlayout.compose.ConstraintSetRef to, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit> transitionContent);
}
diff --git a/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt b/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
new file mode 100644
index 0000000..fc0af6f
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/integration-tests/constraintlayout-compose-demos/src/main/java/androidx/constraintlayout/compose/demos/CustomKeyAttributesDemo.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalMotionApi::class)
+
+package androidx.constraintlayout.compose.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.Dimension
+import androidx.constraintlayout.compose.ExperimentalMotionApi
+import androidx.constraintlayout.compose.MotionLayout
+import androidx.constraintlayout.compose.MotionLayoutFlag
+import androidx.constraintlayout.compose.MotionScene
+import androidx.constraintlayout.compose.OnSwipe
+import androidx.constraintlayout.compose.SwipeDirection
+import androidx.constraintlayout.compose.SwipeSide
+
+@Preview
+@Composable
+fun CustomColorInKeyAttributesDemo() {
+ val boxId = "box"
+ val textId = "text"
+ MotionLayout(
+ motionScene = MotionScene {
+ val box = createRefFor(boxId)
+ val text = createRefFor(textId)
+ defaultTransition(
+ from = constraintSet {
+ constrain(text) {
+ top.linkTo(box.bottom, 10.dp)
+ start.linkTo(box.start)
+ }
+ constrain(box) {
+ width = Dimension.value(50.dp)
+ height = Dimension.value(50.dp)
+
+ centerVerticallyTo(parent)
+ start.linkTo(parent.start)
+
+ customColor("background", Color.Red)
+ }
+ },
+ to = constraintSet {
+ constrain(text) {
+ top.linkTo(box.bottom, 10.dp)
+ end.linkTo(box.end)
+ }
+ constrain(box) {
+ width = Dimension.value(50.dp)
+ height = Dimension.value(50.dp)
+
+ centerVerticallyTo(parent)
+ end.linkTo(parent.end)
+
+ customColor("background", Color.Blue)
+ }
+ }
+ ) {
+ >
+ anchor = box,
+ side = SwipeSide.Middle,
+ direction = SwipeDirection.End
+ )
+ keyAttributes(box) {
+ frame(33) {
+ customColor("background", Color.Yellow)
+ }
+ frame(66) {
+ customColor("background", Color.Green)
+ }
+ }
+ }
+ },
+ progress = 0f,
+ motionLayoutFlags = setOf(MotionLayoutFlag.FullMeasure), // Needed to update text composable
+ modifier = Modifier.fillMaxSize()
+ ) {
+ val background = motionColor(boxId, "background")
+ Box(modifier = Modifier.layoutId(boxId).background(background))
+ Text(
+ modifier = Modifier.layoutId(textId),
+ text = "Color: ${background.toArgb().toUInt().toString(16)}"
+ )
+ }
+}
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
index b6e1f7d..980a920 100644
--- a/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidAndroidTest/kotlin/androidx/constraintlayout/compose/MotionLayoutTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.LocalTextStyle
@@ -29,23 +30,27 @@
import androidx.compose.material.icons.filled.Person
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.size
+import androidx.compose.ui.unit.sp
import androidx.constraintlayout.compose.test.R
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -54,6 +59,7 @@
import org.junit.Test
import org.junit.runner.RunWith
+@OptIn(ExperimentalMotionApi::class)
@MediumTest
@RunWith(AndroidJUnit4::class)
internal class MotionLayoutTest {
@@ -93,6 +99,78 @@
assertEquals(35.dp.value, usernameSize.width.value, absoluteTolerance = 0.5f)
assertEquals(17.dp.value, usernameSize.height.value, absoluteTolerance = 0.5f)
}
+
+ @Test
+ fun testCustomKeyFrameAttributes() {
+ val progress: MutableState<Float> = mutableStateOf(0f)
+ rule.setContent {
+ MotionLayout(
+ motionScene = MotionScene {
+ val element = createRefFor("element")
+ defaultTransition(
+ from = constraintSet {
+ constrain(element) {
+ customColor("color", Color.White)
+ customDistance("distance", 0.dp)
+ customFontSize("fontSize", 0.sp)
+ customInt("int", 0)
+ }
+ },
+ to = constraintSet {
+ constrain(element) {
+ customColor("color", Color.Black)
+ customDistance("distance", 10.dp)
+ customFontSize("fontSize", 20.sp)
+ customInt("int", 30)
+ }
+ }
+ ) {
+ keyAttributes(element) {
+ frame(50) {
+ customColor("color", Color.Red)
+ customDistance("distance", 20.dp)
+ customFontSize("fontSize", 30.sp)
+ customInt("int", 40)
+ }
+ }
+ }
+ },
+ progress = progress.value,
+ modifier = Modifier.size(200.dp)
+ ) {
+ val props by motionProperties(id = "element")
+ Column(Modifier.layoutId("element")) {
+ Text(
+ text = "Color: #${props.color("color").toArgb().toUInt().toString(16)}"
+ )
+ Text(
+ text = "Distance: ${props.distance("distance")}"
+ )
+ Text(
+ text = "FontSize: ${props.fontSize("fontSize")}"
+ )
+ Text(
+ text = "Int: ${props.int("int")}"
+ )
+ }
+ }
+ }
+ rule.waitForIdle()
+
+ progress.value = 0.25f
+ rule.waitForIdle()
+ rule.onNodeWithText("Color: #ffffbaba").assertExists()
+ rule.onNodeWithText("Distance: 10.0.dp").assertExists()
+ rule.onNodeWithText("FontSize: 15.0.sp").assertExists()
+ rule.onNodeWithText("Int: 20").assertExists()
+
+ progress.value = 0.75f
+ rule.waitForIdle()
+ rule.onNodeWithText("Color: #ffba0000").assertExists()
+ rule.onNodeWithText("Distance: 15.0.dp").assertExists()
+ rule.onNodeWithText("FontSize: 25.0.sp").assertExists()
+ rule.onNodeWithText("Int: 35").assertExists()
+ }
}
@OptIn(ExperimentalMotionApi::class)
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
index b5b9dc5..7af1fcc 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionSceneScope.kt
@@ -21,6 +21,8 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.core.state.CorePixelDp
@@ -252,6 +254,69 @@
state.constraints(id).addCustomColor(name, value.toArgb())
}
}
+
+ /**
+ * Declare a custom Int [value] addressed by [name].
+ */
+ fun ConstrainScope.customInt(name: String, value: Int) {
+ tasks.add { state ->
+ state.constraints(id).addCustomFloat(name, value.toFloat())
+ }
+ }
+
+ /**
+ * Declare a custom Dp [value] addressed by [name].
+ */
+ fun ConstrainScope.customDistance(name: String, value: Dp) {
+ tasks.add { state ->
+ state.constraints(id).addCustomFloat(name, value.value)
+ }
+ }
+
+ /**
+ * Declare a custom TextUnit [value] addressed by [name].
+ */
+ fun ConstrainScope.customFontSize(name: String, value: TextUnit) {
+ tasks.add { state ->
+ state.constraints(id).addCustomFloat(name, value.value)
+ }
+ }
+
+ /**
+ * Sets the custom Float [value] at the frame of the current [KeyAttributeScope].
+ */
+ fun KeyAttributeScope.customFloat(name: String, value: Float) {
+ customPropertiesValue[name] = value
+ }
+
+ /**
+ * Sets the custom Color [value] at the frame of the current [KeyAttributeScope].
+ */
+ fun KeyAttributeScope.customColor(name: String, value: Color) {
+ // Colors must be in the following format: "#AARRGGBB"
+ customPropertiesValue[name] = "#${value.toArgb().toUInt().toString(16)}"
+ }
+
+ /**
+ * Sets the custom Int [value] at the frame of the current [KeyAttributeScope].
+ */
+ fun KeyAttributeScope.customInt(name: String, value: Int) {
+ customPropertiesValue[name] = value
+ }
+
+ /**
+ * Sets the custom Dp [value] at the frame of the current [KeyAttributeScope].
+ */
+ fun KeyAttributeScope.customDistance(name: String, value: Dp) {
+ customPropertiesValue[name] = value.value
+ }
+
+ /**
+ * Sets the custom TextUnit [value] at the frame of the current [KeyAttributeScope].
+ */
+ fun KeyAttributeScope.customFontSize(name: String, value: TextUnit) {
+ customPropertiesValue[name] = value.value
+ }
}
data class ConstraintSetRef internal constructor(
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
index 34b560a..b74f5e4 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/TransitionScope.kt
@@ -264,18 +264,48 @@
@ExperimentalMotionApi
abstract class BaseKeyFrameScope internal constructor() {
- protected val userAttributes = mutableMapOf<String, Any>()
+ /**
+ * PropertyName-Value map for the properties of each type of key frame.
+ *
+ * The values are for a singular unspecified frame.
+ */
+ private val keyFramePropertiesValue = mutableMapOf<String, Any>()
+ /**
+ * PropertyName-Value map for user-defined values.
+ *
+ * Typically used on KeyAttributes only.
+ */
+ internal val customPropertiesValue = mutableMapOf<String, Any>()
+
+ /**
+ * When changed, updates the value of type [T] on the [keyFramePropertiesValue] map.
+ *
+ * Where the Key is the property's name unless [nameOverride] is not null.
+ */
protected fun <T> addOnPropertyChange(initialValue: T, nameOverride: String? = null) =
object : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
val name = nameOverride ?: property.name
if (newValue != null) {
- userAttributes[name] = newValue
+ keyFramePropertiesValue[name] = newValue
}
}
}
+ /**
+ * Property delegate that updates the [keyFramePropertiesValue] map on value changes.
+ *
+ * Where the Key is the property's name unless [nameOverride] is not null.
+ *
+ * The value is the String given by [NamedPropertyOrValue.name].
+ *
+ *
+ *
+ * Use when declaring properties that have a named value.
+ *
+ * E.g.: `var curveFit: CurveFit? by addNameOnPropertyChange(null)`
+ */
protected fun <E : NamedPropertyOrValue?> addNameOnPropertyChange(
initialValue: E,
nameOverride: String? = null
@@ -284,14 +314,35 @@
override fun afterChange(property: KProperty<*>, oldValue: E, newValue: E) {
val name = nameOverride ?: property.name
if (newValue != null) {
- userAttributes[name] = newValue.name
+ keyFramePropertiesValue[name] = newValue.name
}
}
}
- fun addToContainer(container: CLContainer) {
- userAttributes.forEach { (name, value) ->
- val array = container.getArrayOrCreate(name)
+ /**
+ * Adds the property maps to the given container.
+ *
+ * Where every value is treated as part of array.
+ */
+ internal fun addToContainer(container: CLContainer) {
+ container.putValuesAsArrayElements(keyFramePropertiesValue)
+ val customPropsObject = container.getObjectOrNull("custom") ?: run {
+ val custom = CLObject(charArrayOf())
+ container.put("custom", custom)
+ custom
+ }
+ customPropsObject.putValuesAsArrayElements(customPropertiesValue)
+ }
+
+ /**
+ * Adds the values from [propertiesSource] to the [CLContainer].
+ *
+ * Each value will be added as a new element of their corresponding array (given by the Key,
+ * which is the name of the affected property).
+ */
+ private fun CLContainer.putValuesAsArrayElements(propertiesSource: Map<String, Any>) {
+ propertiesSource.forEach { (name, value) ->
+ val array = this.getArrayOrCreate(name)
when (value) {
is String -> {
val stringChars = value.toCharArray()
diff --git a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
index ca14558..b93a66a 100644
--- a/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
+++ b/core/core-animation-integration-tests/testapp/src/androidTest/java/androidx/core/animation/AnimatorInflaterTest.java
@@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.content.Context;
@@ -31,9 +32,7 @@
import androidx.test.filters.SmallTest;
import org.junit.ClassRule;
-import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
@SmallTest
@@ -45,9 +44,6 @@
@ClassRule
public static AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
- @Rule
- public final ExpectedException expectedException = ExpectedException.none();
-
@UiThreadTest
@Test
public void testLoadAnimator() {
@@ -122,45 +118,54 @@
@Test
public void pathInterpolator_wrongControlPoint() {
- expectedException.expect(InflateException.class);
- expectedException.expectMessage("requires the controlY1 attribute");
- AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
- R.interpolator.wrong_control_point_interpolator);
+ InflateException exception = assertThrows(InflateException.class, () -> {
+ AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+ R.interpolator.wrong_control_point_interpolator);
+ });
+ assertEquals("pathInterpolator requires the controlY1 attribute", exception.getMessage());
}
@Test
public void pathInterpolator_wrongControlPoints() {
- expectedException.expect(InflateException.class);
- expectedException.expectMessage("requires both controlX2 and controlY2 for cubic Beziers");
- AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
- R.interpolator.wrong_control_points_interpolator);
+
+
+ InflateException exception = assertThrows(InflateException.class, () -> {
+ AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+ R.interpolator.wrong_control_points_interpolator);
+ });
+ assertEquals("pathInterpolator requires both controlX2 and controlY2 for cubic Beziers.",
+ exception.getMessage());
}
@Test
public void pathInterpolator_wrongStartEnd() {
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage("The Path must start at (0,0) and end at (1,1)");
- AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
- R.interpolator.wrong_path_interpolator_1);
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+ R.interpolator.wrong_path_interpolator_1);
+ });
+ assertEquals("The Path must start at (0,0) and end at (1,1)", exception.getMessage());
}
@Test
public void pathInterpolator_loopBack() {
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage("The Path cannot loop back on itself");
- AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
- R.interpolator.wrong_path_interpolator_2);
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+ R.interpolator.wrong_path_interpolator_2);
+ });
+ assertEquals("The Path cannot loop back on itself.", exception.getMessage());
+
}
@Test
public void pathInterpolator_discontinuity() {
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage(Build.VERSION.SDK_INT >= 26
- ? "The Path cannot have discontinuity in the X axis"
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
+ AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
+ R.interpolator.wrong_path_interpolator_3);
+ });
+ assertEquals(Build.VERSION.SDK_INT >= 26
+ ? "The Path cannot have discontinuity in the X axis."
// Older APIs don't detect discontinuity, but they report it as loop back.
- : "The Path cannot loop back on itself");
- AnimatorInflater.loadInterpolator(ApplicationProvider.getApplicationContext(),
- R.interpolator.wrong_path_interpolator_3);
+ : "The Path cannot loop back on itself.", exception.getMessage());
}
static class TestObject {
diff --git a/core/core-location-altitude/api/current.txt b/core/core-location-altitude/api/current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/current.txt
+++ b/core/core-location-altitude/api/current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.core.location.altitude {
+
+ public final class AltitudeConverterCompat {
+ method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+ }
+
+}
+
diff --git a/core/core-location-altitude/api/public_plus_experimental_current.txt b/core/core-location-altitude/api/public_plus_experimental_current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/public_plus_experimental_current.txt
+++ b/core/core-location-altitude/api/public_plus_experimental_current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.core.location.altitude {
+
+ public final class AltitudeConverterCompat {
+ method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+ }
+
+}
+
diff --git a/core/core-location-altitude/api/restricted_current.txt b/core/core-location-altitude/api/restricted_current.txt
index e6f50d0..a333f75 100644
--- a/core/core-location-altitude/api/restricted_current.txt
+++ b/core/core-location-altitude/api/restricted_current.txt
@@ -1 +1,9 @@
// Signature format: 4.0
+package androidx.core.location.altitude {
+
+ public final class AltitudeConverterCompat {
+ method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
+ }
+
+}
+
diff --git a/core/core-location-altitude/build.gradle b/core/core-location-altitude/build.gradle
index e91770c..c081e08 100644
--- a/core/core-location-altitude/build.gradle
+++ b/core/core-location-altitude/build.gradle
@@ -28,6 +28,16 @@
dependencies {
api(libs.kotlinStdlib)
+ api("androidx.annotation:annotation:1.5.0")
+
+ implementation("androidx.concurrent:concurrent-futures:1.1.0")
+ implementation("androidx.core:core:1.9.0")
+
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.truth)
}
androidx {
diff --git a/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java b/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java
new file mode 100644
index 0000000..3c61ed9
--- /dev/null
+++ b/core/core-location-altitude/src/androidTest/java/androidx/core/location/altitude/AltitudeConverterCompatTest.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location.altitude;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.location.Location;
+
+import androidx.arch.core.util.Function;
+import androidx.core.location.LocationCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AltitudeConverterCompatTest {
+
+ private Location mValidLocation;
+ private Context mContext;
+
+ private static void assertEquals(Location actual, Location expected) {
+ assertThat(actual.getLatitude()).isEqualTo(expected.getLatitude());
+ assertThat(actual.getLongitude()).isEqualTo(expected.getLongitude());
+ assertEquals(actual, expected, Location::hasAltitude, Location::getAltitude);
+ assertEquals(
+ actual,
+ expected,
+ LocationCompat::hasVerticalAccuracy,
+ LocationCompat::getVerticalAccuracyMeters);
+ assertEquals(
+ actual, expected, LocationCompat::hasMslAltitude,
+ LocationCompat::getMslAltitudeMeters);
+ assertEquals(
+ actual,
+ expected,
+ LocationCompat::hasMslAltitudeAccuracy,
+ LocationCompat::getMslAltitudeAccuracyMeters);
+ }
+
+ private static <T> void assertEquals(
+ Location actual,
+ Location expected,
+ Function<Location, Boolean> has,
+ Function<Location, T> get) {
+ assertThat(has.apply(actual)).isEqualTo(has.apply(expected));
+ if (has.apply(expected)) {
+ assertThat(get.apply(actual)).isEqualTo(get.apply(expected));
+ }
+ }
+
+ @Before
+ public void setUp() {
+ mValidLocation = new Location("");
+ mValidLocation.setLatitude(-90);
+ mValidLocation.setLongitude(180);
+ mValidLocation.setAltitude(-1);
+ LocationCompat.setVerticalAccuracyMeters(mValidLocation, 1);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ }
+
+ @Test
+ public void testAddMslAltitude_validLocationThrows() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(mValidLocation)));
+ }
+
+ @Test
+ public void testAddMslAltitude_invalidLatitudeThrows() {
+ Location location = new Location(mValidLocation);
+
+ location.setLatitude(Double.NaN);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setLatitude(91);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setLatitude(-91);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+ }
+
+ @Test
+ public void testAddMslAltitude_invalidLongitudeThrows() {
+ Location location = new Location(mValidLocation);
+
+ location.setLongitude(Double.NaN);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setLongitude(181);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setLongitude(-181);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+ }
+
+ @Test
+ public void testAddMslAltitude_invalidAltitudeThrows() {
+ Location location = new Location(mValidLocation);
+
+ location.removeAltitude();
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setAltitude(Double.NaN);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+
+ location.setAltitude(Double.POSITIVE_INFINITY);
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> AltitudeConverterCompat.addMslAltitudeAsync(mContext,
+ new Location(location)));
+ }
+}
diff --git a/core/core-location-altitude/src/main/assets/database/geoids-v0.db b/core/core-location-altitude/src/main/assets/database/geoids-v0.db
new file mode 100644
index 0000000..1be3bd7
--- /dev/null
+++ b/core/core-location-altitude/src/main/assets/database/geoids-v0.db
Binary files differ
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
new file mode 100644
index 0000000..ce18128
--- /dev/null
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.core.location.altitude;
+
+import android.content.Context;
+import android.location.Location;
+
+import androidx.annotation.NonNull;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.Objects;
+
+/**
+ * Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
+ * into ones above Mean Sea Level.
+ */
+public final class AltitudeConverterCompat {
+
+ private static final double MAX_ABS_VALID_LATITUDE = 90;
+ private static final double MAX_ABS_VALID_LONGITUDE = 180;
+
+ /** Prevents instantiation. */
+ private AltitudeConverterCompat() {
+ }
+
+ /**
+ * Returns a {@link ListenableFuture} that, upon success, adds a Mean Sea Level altitude to the
+ * {@code location} in {@link Location#getExtras()}. In addition, adds a Mean Sea Level altitude
+ * accuracy if the {@code location} has a valid vertical accuracy.
+ *
+ * <p>The {@link ListenableFuture} leaves the {@code location} unchanged if and only if any
+ * of the following are true:
+ *
+ * <ul>
+ * <li>the {@code location} has an invalid latitude, longitude, or altitude above WGS84
+ * <li>an I/O error occurs when loading data from raw assets via {@code context}.
+ * </ul>
+ *
+ * <p>NOTE: Currently throws {@link UnsupportedOperationException} for a valid {@code location}.
+ */
+ @NonNull
+ public static ListenableFuture<Location> addMslAltitudeAsync(
+ @NonNull Context context,
+ @NonNull Location location) {
+ Objects.requireNonNull(context);
+ Objects.requireNonNull(location);
+ validate(location);
+ throw new UnsupportedOperationException("addMslAltitude method not implemented.");
+ }
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the {@code location} has an invalid latitude,
+ * longitude, or altitude above WGS84.
+ */
+ private static void validate(@NonNull Location location) {
+ Preconditions.checkArgument(isFiniteAndAtAbsMost(location.getLatitude(),
+ MAX_ABS_VALID_LATITUDE), "Location must contain a valid latitude.");
+ Preconditions.checkArgument(isFiniteAndAtAbsMost(location.getLongitude(),
+ MAX_ABS_VALID_LONGITUDE), "Location must contain a valid longitude.");
+ Preconditions.checkArgument(location.hasAltitude() && isFinite(location.getAltitude()),
+ "Location must contain a valid altitude above WGS84.");
+ }
+
+ private static boolean isFiniteAndAtAbsMost(double value, double rhs) {
+ return isFinite(value) && Math.abs(value) <= rhs;
+ }
+
+ private static boolean isFinite(double value) {
+ return !Double.isInfinite(value) && !Double.isNaN(value);
+ }
+}
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
index 25a66c6..27d75f7 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestController.kt
@@ -25,10 +25,10 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.splashscreen.SplashScreenViewProvider
-import androidx.test.runner.screenshot.Screenshot
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import androidx.core.splashscreen.R as SR
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
/**
* If true, sets an [androidx.core.splashscreen.SplashScreen.OnExitAnimationListener] on the
@@ -124,7 +124,7 @@
waitedLatch.countDown()
val shouldWait = waitBarrier.get() || waitedLatch.count > 0L
if (!shouldWait && takeScreenShot && splashScreenScreenshot == null) {
- splashScreenScreenshot = Screenshot.capture().bitmap
+ splashScreenScreenshot = getInstrumentation().uiAutomation.takeScreenshot()
}
shouldWait
}
@@ -141,7 +141,8 @@
if (takeScreenShot) {
splashScreenViewProvider.view.postDelayed(
{
- splashScreenViewScreenShot = Screenshot.capture().bitmap
+ splashScreenViewScreenShot =
+ getInstrumentation().uiAutomation.takeScreenshot()
splashScreenViewProvider.remove()
exitAnimationListenerLatch.countDown()
},
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
index 4d9d38d..ab7c012 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashScreenTestUtils.kt
@@ -27,6 +27,7 @@
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.hamcrest.core.IsNull
+import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert
import kotlin.reflect.KClass
@@ -49,7 +50,7 @@
// Wait for launcher
val launcherPackage: String = device.launcherPackageName
- Assert.assertThat(launcherPackage, IsNull.notNullValue())
+ assertThat(launcherPackage, IsNull.notNullValue())
device.wait(
Until.hasObject(By.pkg(launcherPackage).depth(0)),
LAUNCH_TIMEOUT
diff --git a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
index ba48c48..e84532f 100644
--- a/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
+++ b/core/core-splashscreen/src/androidTest/java/androidx/core/splashscreen/test/SplashscreenParametrizedTest.kt
@@ -22,11 +22,10 @@
import android.view.View
import android.view.ViewTreeObserver
import androidx.core.splashscreen.SplashScreenViewProvider
+import androidx.test.core.app.takeScreenshot
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.screenshot.ScreenCapture
-import androidx.test.runner.screenshot.Screenshot
import androidx.test.screenshot.matchers.MSSIMMatcher
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
@@ -179,7 +178,7 @@
}
assertTrue(controller.drawnLatch.await(2, TimeUnit.SECONDS))
Thread.sleep(500)
- val withoutListener = Screenshot.capture()
+ val withoutListener = takeScreenshot()
// Take a screenshot of the container view while the splash screen view is invisible but
// not removed
@@ -191,7 +190,7 @@
}
val withListener = screenshotContainerInExitListener(controller)
- compareBitmaps(withoutListener.bitmap, withListener.bitmap, 0.999)
+ compareBitmaps(withoutListener, withListener, 0.999)
// Execute the same steps as above but with another set of theme attributes to check.
controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
@@ -201,7 +200,7 @@
}
controller.waitForActivityDrawn()
Thread.sleep(500)
- val withoutListener2 = Screenshot.capture()
+ val withoutListener2 = takeScreenshot()
controller = startActivityWithSplashScreen(SplashScreenStability2::class, device) {
// Clear out any previous instances
@@ -210,17 +209,17 @@
it.putExtra(EXTRA_SPLASHSCREEN_WAIT, true)
}
val withListener2 = screenshotContainerInExitListener(controller)
- compareBitmaps(withListener2.bitmap, withoutListener2.bitmap)
+ compareBitmaps(withListener2, withoutListener2)
}
private fun screenshotContainerInExitListener(
controller: SplashScreenTestController
- ): ScreenCapture {
- lateinit var contentViewInListener: ScreenCapture
+ ): Bitmap {
+ lateinit var contentViewInListener: Bitmap
controller.doOnExitAnimation {
it.view.visibility = View.INVISIBLE
it.view.postDelayed({
- contentViewInListener = Screenshot.capture()
+ contentViewInListener = takeScreenshot()
it.remove()
controller.exitAnimationListenerLatch.countDown()
}, 100)
@@ -373,4 +372,4 @@
exitAnimationListenerLatch.await(2, TimeUnit.SECONDS)
)
}
-}
\ No newline at end of file
+}
diff --git a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
index 41382a3..041e5ab 100644
--- a/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/NotificationCompatTest.java
@@ -228,7 +228,21 @@
assertEquals("testSubText", NotificationCompat.getSubText(n));
}
- @FlakyTest(bugId = 190533219)
+ @SdkSuppress(maxSdkVersion = 15)
+ @Test
+ public void testActionsUnsupported() {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
+ Notification nWith = builder.addAction(0, "testAction", null).build();
+ // Version prior to API 16 do not support Actions.
+ assertEquals(0, NotificationCompat.getActionCount(nWith));
+ NotificationCompat.Action action = NotificationCompat.getAction(nWith, 0);
+ assertNull(action);
+
+ Notification nWithout = builder.clearActions().build();
+ assertEquals(0, NotificationCompat.getActionCount(nWithout));
+ }
+
+ @SdkSuppress(minSdkVersion = 16)
@Test
public void testActions() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
diff --git a/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java b/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
index 38c1daa..7465cb3 100644
--- a/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/PermissionCheckerTest.java
@@ -23,10 +23,10 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.FlakyTest;
import androidx.test.filters.SmallTest;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -43,7 +43,7 @@
mContext = ApplicationProvider.getApplicationContext();
}
- @FlakyTest(bugId = 242739867)
+ @Ignore // b/242739867
@Test
public void testCheckPermission() throws Exception {
assertEquals(PermissionChecker.PERMISSION_DENIED, PermissionChecker.checkSelfPermission(
diff --git a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
index 298c8d0..9492383 100644
--- a/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
+++ b/core/core/src/androidTest/java/androidx/core/view/WindowInsetsControllerCompatActivityTest.kt
@@ -226,6 +226,7 @@
testShow(WindowInsetsCompat.Type.statusBars())
}
+ @Ignore // b/244445899
@Test
public fun toggle_NavBar() {
testHide(WindowInsetsCompat.Type.navigationBars())
diff --git a/credentials/credentials-play-services-auth/OWNERS b/credentials/credentials-play-services-auth/OWNERS
new file mode 100644
index 0000000..7b0e47f
--- /dev/null
+++ b/credentials/credentials-play-services-auth/OWNERS
@@ -0,0 +1,8 @@
+# Bug Component: 1218609
+sgjerry@google.com
+helenqin@google.com
+reemabajwa@google.com
+duqinmei@google.com
+beccahughes@google.com
+leecam@google.com
+akaphle@google.com
diff --git a/credentials/credentials-play-services-auth/build.gradle b/credentials/credentials-play-services-auth/build.gradle
index 725338d..430b672 100644
--- a/credentials/credentials-play-services-auth/build.gradle
+++ b/credentials/credentials-play-services-auth/build.gradle
@@ -26,7 +26,22 @@
api(libs.kotlinStdlib)
api project(":credentials:credentials")
- // Add dependencies here
+ implementation project(path: ':activity:activity')
+ // Closed source dependencies
+ implementation(libs.playServicesAuth) {
+ exclude group: "androidx.loader"
+ exclude group: "androidx.fragment"
+ }
+ implementation(libs.playServicesFido)
+
+ androidTestImplementation(libs.junit)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.truth)
+ androidTestImplementation(project(":internal-testutils-truth"))
+ androidTestImplementation(libs.kotlinCoroutinesAndroid)
}
android {
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
index 59ad679..6e59651 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/CredentialProviderPlayServicesImpl.kt
@@ -18,12 +18,16 @@
import android.app.Activity
import android.os.CancellationSignal
+import android.util.Log
import androidx.credentials.CreateCredentialRequest
import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CreatePublicKeyCredentialRequest
import androidx.credentials.CredentialManagerCallback
import androidx.credentials.CredentialProvider
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetCredentialResponse
+import androidx.credentials.playservices.controllers.CreatePassword.CredentialProviderCreatePasswordController
import java.util.concurrent.Executor
/**
@@ -32,6 +36,7 @@
*
* @hide
*/
+@Suppress("deprecation")
class CredentialProviderPlayServicesImpl : CredentialProvider {
override fun onGetCredential(
request: GetCredentialRequest,
@@ -40,6 +45,10 @@
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse>
) {
+ if (cancellationSignal != null) {
+ Log.i(TAG, "onCreateCredential cancellationSignal not used")
+ TODO("Use Cancel Operations Properly")
+ }
TODO("Not yet implemented")
}
@@ -50,10 +59,31 @@
executor: Executor,
callback: CredentialManagerCallback<CreateCredentialResponse>
) {
- TODO("Not yet implemented")
+ if (cancellationSignal != null) {
+ Log.i(TAG, "onCreateCredential cancellationSignal not used")
+ TODO("Use Cancel Operations Properly")
+ }
+ val fragmentManager: android.app.FragmentManager = activity!!.fragmentManager
+ // TODO("Manage Fragment Lifecycle and Fragment Manager Properly")
+ if (request is CreatePasswordRequest) {
+ CredentialProviderCreatePasswordController.getInstance(
+ fragmentManager).invokePlayServices(
+ request,
+ callback,
+ executor)
+ } else if (request is CreatePublicKeyCredentialRequest) {
+ TODO("Not yet implemented")
+ } else {
+ throw UnsupportedOperationException(
+ "Unsupported request; not password or publickeycredential")
+ }
}
override fun isAvailableOnDevice(): Boolean {
TODO("Not yet implemented")
}
+
+ companion object {
+ private val TAG = CredentialProviderPlayServicesImpl::class.java.name
+ }
}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
new file mode 100644
index 0000000..994429a
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/BeginSignIn/CredentialProviderBeginSignInController.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.BeginSignIn
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.GetCredentialRequest
+import androidx.credentials.GetCredentialResponse
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.BeginSignInRequest
+import com.google.android.gms.auth.api.identity.SignInCredential
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the BeginSignIn flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderBeginSignInController : CredentialProviderController<
+ GetCredentialRequest,
+ BeginSignInRequest,
+ SignInCredential,
+ GetCredentialResponse>() {
+
+ /**
+ * The callback object state, used in the protected handleResponse method.
+ */
+ private lateinit var callback: CredentialManagerCallback<GetCredentialResponse>
+ /**
+ * The callback requires an executor to invoke it.
+ */
+ private lateinit var executor: Executor
+
+ override fun invokePlayServices(
+ request: GetCredentialRequest,
+ callback: CredentialManagerCallback<GetCredentialResponse>,
+ executor: Executor
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ handleResponse(requestCode, resultCode, data)
+ }
+
+ private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+ Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToPlayServices(request: GetCredentialRequest): BeginSignInRequest {
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToCredentialProvider(response: SignInCredential): GetCredentialResponse {
+ TODO("Not yet implemented")
+ }
+
+ companion object {
+ private val TAG = CredentialProviderBeginSignInController::class.java.name
+ private const val REQUEST_CODE_BEGIN_SIGN_IN: Int = 1
+ // TODO("Ensure this works with the lifecycle")
+
+ /**
+ * This finds a past version of the BeginSignInController if it exists, otherwise
+ * it generates a new instance.
+ *
+ * @param fragmentManager a fragment manager pulled from an android activity
+ * @return a credential provider controller for a specific credential request
+ */
+ @JvmStatic
+ fun getInstance(fragmentManager: android.app.FragmentManager):
+ CredentialProviderBeginSignInController {
+ var controller = findPastController(REQUEST_CODE_BEGIN_SIGN_IN, fragmentManager)
+ if (controller == null) {
+ controller = CredentialProviderBeginSignInController()
+ fragmentManager.beginTransaction().add(controller,
+ REQUEST_CODE_BEGIN_SIGN_IN.toString())
+ .commitAllowingStateLoss()
+ fragmentManager.executePendingTransactions()
+ }
+ return controller
+ }
+
+ internal fun findPastController(
+ requestCode: Int,
+ fragmentManager: android.app.FragmentManager
+ ): CredentialProviderBeginSignInController? {
+ try {
+ return fragmentManager.findFragmentByTag(requestCode.toString())
+ as CredentialProviderBeginSignInController?
+ } catch (e: Exception) {
+ Log.i(TAG, "Old fragment found of different type - replacement required")
+ // TODO("Ensure this is well tested for fragment issues")
+ return null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
new file mode 100644
index 0000000..7015a9a
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePassword/CredentialProviderCreatePasswordController.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.CreatePassword
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePasswordRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.auth.api.identity.SavePasswordRequest
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the CreatePassword flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderCreatePasswordController : CredentialProviderController<
+ CreatePasswordRequest,
+ SavePasswordRequest,
+ Intent,
+ CreateCredentialResponse>() {
+
+ /**
+ * The callback object state, used in the protected handleResponse method.
+ */
+ private lateinit var callback: CredentialManagerCallback<CreateCredentialResponse>
+
+ /**
+ * The callback requires an executor to invoke it.
+ */
+ private lateinit var executor: Executor
+
+ override fun invokePlayServices(
+ request: CreatePasswordRequest,
+ callback: CredentialManagerCallback<CreateCredentialResponse>,
+ executor: Executor
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ handleResponse(requestCode, resultCode, data)
+ }
+
+ private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+ Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToPlayServices(request: CreatePasswordRequest): SavePasswordRequest {
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToCredentialProvider(response: Intent): CreateCredentialResponse {
+ TODO("Not yet implemented")
+ }
+
+ companion object {
+ private val TAG = CredentialProviderCreatePasswordController::class.java.name
+ private const val REQUEST_CODE_GIS_SAVE_PASSWORD: Int = 1
+ // TODO("Ensure this works with the lifecycle")
+
+ /**
+ * This finds a past version of the
+ * [CredentialProviderCreatePasswordController] if it exists, otherwise
+ * it generates a new instance.
+ *
+ * @param fragmentManager a fragment manager pulled from an android activity
+ * @return a credential provider controller for CreatePublicKeyCredential
+ */
+ @JvmStatic
+ fun getInstance(fragmentManager: android.app.FragmentManager):
+ CredentialProviderCreatePasswordController {
+ var controller = findPastController(REQUEST_CODE_GIS_SAVE_PASSWORD, fragmentManager)
+ if (controller == null) {
+ controller = CredentialProviderCreatePasswordController()
+ fragmentManager.beginTransaction().add(controller,
+ REQUEST_CODE_GIS_SAVE_PASSWORD.toString())
+ .commitAllowingStateLoss()
+ fragmentManager.executePendingTransactions()
+ }
+ return controller
+ }
+
+ internal fun findPastController(
+ requestCode: Int,
+ fragmentManager: android.app.FragmentManager
+ ): CredentialProviderCreatePasswordController? {
+ try {
+ return fragmentManager.findFragmentByTag(requestCode.toString())
+ as CredentialProviderCreatePasswordController
+ } catch (e: Exception) {
+ Log.i(TAG, "Old fragment found of different type - replacement required")
+ // TODO("Ensure this is well tested for fragment issues")
+ return null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
new file mode 100644
index 0000000..d6e8763
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CreatePublicKeyCredential/CredentialProviderCreatePublicKeyCredentialController.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers.CreatePublicKeyCredential
+
+import android.content.Intent
+import android.util.Log
+import androidx.credentials.CreateCredentialResponse
+import androidx.credentials.CreatePublicKeyCredentialRequest
+import androidx.credentials.CredentialManagerCallback
+import androidx.credentials.playservices.controllers.CredentialProviderController
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredential
+import com.google.android.gms.fido.fido2.api.common.PublicKeyCredentialCreationOptions
+import java.util.concurrent.Executor
+
+/**
+ * A controller to handle the CreatePublicKeyCredential flow with play services.
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+class CredentialProviderCreatePublicKeyCredentialController :
+ CredentialProviderController<
+ CreatePublicKeyCredentialRequest,
+ PublicKeyCredentialCreationOptions,
+ PublicKeyCredential,
+ CreateCredentialResponse>() {
+
+ /**
+ * The callback object state, used in the protected handleResponse method.
+ */
+ private lateinit var callback: CredentialManagerCallback<CreateCredentialResponse>
+
+ /**
+ * The callback requires an executor to invoke it.
+ */
+ private lateinit var executor: Executor
+
+ override fun invokePlayServices(
+ request: CreatePublicKeyCredentialRequest,
+ callback: CredentialManagerCallback<CreateCredentialResponse>,
+ executor: Executor
+ ) {
+ TODO("Not yet implemented")
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ handleResponse(requestCode, resultCode, data)
+ }
+
+ private fun handleResponse(uniqueRequestCode: Int, resultCode: Int, data: Intent?) {
+ Log.i(TAG, "$uniqueRequestCode $resultCode $data")
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToPlayServices(request: CreatePublicKeyCredentialRequest):
+ PublicKeyCredentialCreationOptions {
+ TODO("Not yet implemented")
+ }
+
+ override fun convertToCredentialProvider(response: PublicKeyCredential):
+ CreateCredentialResponse {
+ TODO("Not yet implemented")
+ }
+
+ companion object {
+ private val TAG = CredentialProviderCreatePublicKeyCredentialController::class.java.name
+ private const val REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL: Int = 1
+ // TODO("Ensure this works with the lifecycle")
+
+ /**
+ * This finds a past version of the
+ * [CredentialProviderCreatePublicKeyCredentialController] if it exists, otherwise
+ * it generates a new instance.
+ *
+ * @param fragmentManager a fragment manager pulled from an android activity
+ * @return a credential provider controller for CreatePublicKeyCredential
+ */
+ @JvmStatic
+ fun getInstance(fragmentManager: android.app.FragmentManager):
+ CredentialProviderCreatePublicKeyCredentialController {
+ var controller = findPastController(
+ REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL,
+ fragmentManager)
+ if (controller == null) {
+ controller = CredentialProviderCreatePublicKeyCredentialController()
+ fragmentManager.beginTransaction().add(controller,
+ REQUEST_CODE_GIS_SAVE_PUBLIC_KEY_CREDENTIAL.toString())
+ .commitAllowingStateLoss()
+ fragmentManager.executePendingTransactions()
+ }
+ return controller
+ }
+
+ internal fun findPastController(
+ requestCode: Int,
+ fragmentManager: android.app.FragmentManager
+ ): CredentialProviderCreatePublicKeyCredentialController? {
+ try {
+ return fragmentManager.findFragmentByTag(requestCode.toString())
+ as CredentialProviderCreatePublicKeyCredentialController
+ } catch (e: Exception) {
+ Log.i(TAG, "Old fragment found of different type - replacement required")
+ // TODO("Ensure this is well tested for fragment issues")
+ return null
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
new file mode 100644
index 0000000..12ab060
--- /dev/null
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.playservices.controllers
+
+import androidx.credentials.CredentialManagerCallback
+import java.util.concurrent.Executor
+
+/**
+ * Extensible abstract class for credential controllers. Please implement this class per every
+ * request/response credential type. Unique logic is left to the use case of the implementation.
+ * If you are building your own version as an OEM, the below can be mimicked to your own
+ * credential provider equivalent and whatever internal service you invoke.
+ *
+ * @param T1 the credential request type from credential manager
+ * @param T2 the credential request type converted to play services
+ * @param R2 the credential response type from play services
+ * @param R1 the credential response type converted back to that used by credential manager
+ *
+ * @hide
+ */
+@Suppress("deprecation")
+abstract class CredentialProviderController<T1 : Any, T2 : Any, R2 : Any, R1 : Any> : android.app
+ .Fragment() {
+
+ /**
+ * Invokes the flow that starts retrieving credential data. In this use case, we invoke
+ * play service modules.
+ *
+ * @param request a credential provider request
+ * @param callback a credential manager callback with a credential provider response
+ * @param executor to be used in any multi-threaded operation calls, such as listenable futures
+ */
+ abstract fun invokePlayServices(
+ request: T1,
+ callback: CredentialManagerCallback<R1>,
+ executor: Executor
+ )
+
+ /**
+ * Allows converting from a credential provider request to a play service request.
+ *
+ * @param request a credential provider request
+ * @return a play service request
+ */
+ protected abstract fun convertToPlayServices(request: T1): T2
+
+ /**
+ * Allows converting from a play service response to a credential provider response.
+ *
+ * @param response a play service response
+ * @return a credential provider response
+ */
+ protected abstract fun convertToCredentialProvider(response: R2): R1
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
index c0c51a0..0be5f0c 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialProvider.kt
@@ -55,7 +55,7 @@
*/
fun onGetCredential(
request: GetCredentialRequest,
- activity: Activity?,
+ activity: Activity?, // TODO("Update on optionality")
cancellationSignal: CancellationSignal?,
executor: Executor,
callback: CredentialManagerCallback<GetCredentialResponse>,
diff --git a/datastore/datastore-core-okio/build.gradle b/datastore/datastore-core-okio/build.gradle
index 5353b43..3f9c98a 100644
--- a/datastore/datastore-core-okio/build.gradle
+++ b/datastore/datastore-core-okio/build.gradle
@@ -27,7 +27,9 @@
def enableNative = KmpPlatformsKt.enableNative(project)
androidXMultiplatform {
- jvm()
+ jvm() {
+ withJava()
+ }
mac()
linux()
ios()
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index 26a1921..b5194bd 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -27,7 +27,9 @@
def enableNative = KmpPlatformsKt.enableNative(project)
androidXMultiplatform {
- jvm()
+ jvm() {
+ withJava()
+ }
mac()
linux()
ios()
diff --git a/datastore/datastore-multiprocess/build.gradle b/datastore/datastore-multiprocess/build.gradle
index 3d340db..e59a2cf 100644
--- a/datastore/datastore-multiprocess/build.gradle
+++ b/datastore/datastore-multiprocess/build.gradle
@@ -59,9 +59,6 @@
}
android {
- defaultConfig {
- minSdkVersion 19
- }
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
index 8199310..f8b2f2e 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
@@ -60,6 +60,7 @@
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
@@ -244,6 +245,7 @@
}
}
+ @Ignore("b/244352517")
@Test
fun testReadFromNonExistentFile() = runTest {
val nonExistentFile = tempFolder.newFile()
@@ -997,4 +999,4 @@
throw IOException("Handler thrown exception.")
}
}
-}
\ No newline at end of file
+}
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
index 3dc1b02..99af5fe 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
@@ -27,6 +27,7 @@
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
+import java.nio.channels.FileLock
import java.util.concurrent.Semaphore
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext
@@ -139,6 +140,8 @@
private val VERSION_SUFFIX = ".version"
private val BUG_MESSAGE = "This is a bug in DataStore. Please file a bug at: " +
"https://issuetracker.google.com/issues/new?component=907884&template=1466542"
+ // TODO(b/255419657): update the shared lock IOException handling logic
+ private val LOCK_ERROR_MESSAGE = "fcntl failed: EAGAIN"
private val INVALID_VERSION = -1
private var initTasks: List<suspend (api: InitializerApi<T>) -> Unit>? =
initTasksList.toList()
@@ -527,11 +530,14 @@
// semaphore to make the best use of coroutine
ThreadLock.acquire(threadLockSemaphore).use {
FileOutputStream(lockFile).use { lockFileStream ->
- lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
- .use { _ ->
- val data = block()
- return Data(data, data.hashCode(), sharedCounter.getValue())
- }
+ var lock: FileLock? = null
+ try {
+ lock = lockFileStream.getChannel().lock(0L, Long.MAX_VALUE, /* shared= */ false)
+ val data = block()
+ return Data(data, data.hashCode(), sharedCounter.getValue())
+ } finally {
+ lock?.release()
+ }
}
}
}
@@ -544,10 +550,30 @@
return block(false)
}
FileInputStream(lockFile).use { lockFileStream ->
- lockFileStream.getChannel().tryLock(0L, Long.MAX_VALUE, /* shared= */ true)
- .use { lock ->
- return block(lock != null)
+ var lock: FileLock? = null
+ try {
+ try {
+ lock = lockFileStream.getChannel().tryLock(
+ /* position= */ 0L,
+ /* size= */ Long.MAX_VALUE,
+ /* shared= */ true)
+ } catch (ex: IOException) {
+ // TODO(b/255419657): Update the shared lock IOException handling logic for
+ // KMM.
+
+ // Some platforms / OS do not support shared lock and convert shared lock
+ // requests to exclusive lock requests. If the lock can't be acquired, it
+ // will throw an IOException with EAGAIN error, instead of returning null as
+ // specified in {@link FileChannel#tryLock}. We only continue if the error
+ // message is EAGAIN, otherwise just throw it.
+ if (ex.message?.startsWith(LOCK_ERROR_MESSAGE) != true) {
+ throw ex
+ }
}
+ return block(lock != null)
+ } finally {
+ lock?.release()
+ }
}
}
}
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index e01662d..1442640 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -75,11 +75,15 @@
internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
val file = produceFile()
- return ParcelFileDescriptor.open(
- file,
- ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
- ).use {
- createCounterFromFd(it, enableMlock)
+ var pfd: ParcelFileDescriptor? = null
+ try {
+ pfd = ParcelFileDescriptor.open(
+ file,
+ ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
+ )
+ return createCounterFromFd(pfd, enableMlock)
+ } finally {
+ pfd?.close()
}
}
}
diff --git a/datastore/datastore-preferences-core/api/current.txt b/datastore/datastore-preferences-core/api/current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/current.txt
+++ b/datastore/datastore-preferences-core/api/current.txt
@@ -62,5 +62,13 @@
method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
}
+ public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+ method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ property public androidx.datastore.preferences.core.Preferences defaultValue;
+ field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+ }
+
}
diff --git a/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt b/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-preferences-core/api/public_plus_experimental_current.txt
@@ -62,5 +62,13 @@
method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
}
+ public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+ method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ property public androidx.datastore.preferences.core.Preferences defaultValue;
+ field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+ }
+
}
diff --git a/datastore/datastore-preferences-core/api/restricted_current.txt b/datastore/datastore-preferences-core/api/restricted_current.txt
index f76fc6f..cdc92af 100644
--- a/datastore/datastore-preferences-core/api/restricted_current.txt
+++ b/datastore/datastore-preferences-core/api/restricted_current.txt
@@ -62,5 +62,13 @@
method public static suspend Object? edit(androidx.datastore.core.DataStore<androidx.datastore.preferences.core.Preferences>, kotlin.jvm.functions.Function2<? super androidx.datastore.preferences.core.MutablePreferences,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> transform, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>);
}
+ public final class PreferencesSerializer implements androidx.datastore.core.okio.OkioSerializer<androidx.datastore.preferences.core.Preferences> {
+ method public androidx.datastore.preferences.core.Preferences getDefaultValue();
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? readFrom(okio.BufferedSource source, kotlin.coroutines.Continuation<? super androidx.datastore.preferences.core.Preferences>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ method @kotlin.jvm.Throws(exceptionClasses={IOException::class, CorruptionException::class}) public suspend Object? writeTo(androidx.datastore.preferences.core.Preferences t, okio.BufferedSink sink, kotlin.coroutines.Continuation<? super kotlin.Unit>) throws androidx.datastore.core.CorruptionException, java.io.IOException;
+ property public androidx.datastore.preferences.core.Preferences defaultValue;
+ field public static final androidx.datastore.preferences.core.PreferencesSerializer INSTANCE;
+ }
+
}
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index 539d5a9..20f30bf 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -29,7 +29,9 @@
def enableNative = KmpPlatformsKt.enableNative(project)
androidXMultiplatform {
- jvm()
+ jvm() {
+ withJava()
+ }
mac()
linux()
ios()
diff --git a/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
index 6c4ab296..92c0407 100644
--- a/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
+++ b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/Expect.kt
@@ -16,7 +16,6 @@
package androidx.datastore.preferences.core
-import androidx.datastore.core.okio.OkioSerializer
import kotlinx.coroutines.CoroutineDispatcher
internal expect fun <K, V> immutableMap(map: Map<K, V>): Map<K, V>
@@ -29,5 +28,3 @@
fun set(value: Boolean)
fun get(): Boolean
}
-
-internal expect fun getPreferencesSerializer(): OkioSerializer<Preferences>
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
similarity index 60%
rename from room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
rename to datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
index 4e95b3d..d6f5aba 100644
--- a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseKt.java
+++ b/datastore/datastore-preferences-core/src/commonMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
@@ -14,15 +14,13 @@
* limitations under the License.
*/
-package androidx.room;
+package androidx.datastore.preferences.core
-import kotlin.coroutines.Continuation;
-import kotlin.jvm.functions.Function1;
+import androidx.datastore.core.okio.OkioSerializer
-public class RoomDatabaseKt {
- public static final <R> Object withTransaction(RoomDatabase db,
- Function1<? super Continuation<? super R>, ? extends Object> block,
- Continuation<? super R> cont) {
- return null;
- }
-}
\ No newline at end of file
+/**
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
+ */
+expect object PreferencesSerializer : OkioSerializer<Preferences>
diff --git a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
index 6c696a0..8ec132a 100644
--- a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
+++ b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesCompatibilityTest.kt
@@ -16,7 +16,6 @@
package androidx.datastore.preferences.core
-import androidx.datastore.core.okio.OkioSerializer
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
@@ -36,7 +35,6 @@
"gBCgwKBm15TG9uZxICIAEKGQoIbXlTdHJpbmcSDSoLc3RyaW5nVmFsdWUKDwoJbXlCb29sZWFuEgIIAQo" +
"bCgtteVN0cmluZ1NldBIMMgoKA29uZQoDdHdvChMKC215Qnl0ZUFycmF5EgRCAgEC"
val byteString = protoBase64.decodeBase64() ?: throw Exception("Unable to decode")
- val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
val expectedProto = preferencesOf(
Preferences.Pair(floatPreferencesKey("myFloat"), 1.1f),
Preferences.Pair(doublePreferencesKey("myDouble"), 1.1),
@@ -50,7 +48,7 @@
val protoBuffer = Buffer()
protoBuffer.write(byteString)
- val protoPrefsFromBytes = preferencesSerializer.readFrom(protoBuffer)
+ val protoPrefsFromBytes = PreferencesSerializer.readFrom(protoBuffer)
assertEquals(expectedProto, protoPrefsFromBytes)
}
}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
index 185d14f..aa06488 100644
--- a/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
+++ b/datastore/datastore-preferences-core/src/commonTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerTest.kt
@@ -36,7 +36,7 @@
private val testIO = OkioTestIO()
private lateinit var testFile: OkioPath
- private val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
+ private val preferencesSerializer: OkioSerializer<Preferences> = PreferencesSerializer
private val fileSystem: FileSystem = FileSystem.SYSTEM
@BeforeTest
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
index 89dd3e5..aaef346 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
+++ b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/Actual.jvm.kt
@@ -20,7 +20,6 @@
import androidx.annotation.RestrictTo
-import androidx.datastore.core.okio.OkioSerializer
import java.util.Collections
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineDispatcher
@@ -46,7 +45,3 @@
delegate = AtomicBoolean(initialValue)
}
}
-
-internal actual fun getPreferencesSerializer(): OkioSerializer<Preferences> {
- return PreferencesSerializer
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
similarity index 90%
rename from datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
rename to datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
index f92d0c8..c413194 100644
--- a/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.kt
+++ b/datastore/datastore-preferences-core/src/jvmMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.jvm.kt
@@ -29,10 +29,12 @@
import okio.IOException
/**
- * Proto based serializer for Preferences.
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
*/
-internal object PreferencesSerializer : OkioSerializer<Preferences> {
- val fileExtension = "preferences_pb"
+actual object PreferencesSerializer : OkioSerializer<Preferences> {
+ internal const val fileExtension = "preferences_pb"
override val defaultValue: Preferences
get() {
@@ -52,6 +54,7 @@
return mutablePreferences.toPreferences()
}
+ @Suppress("InvalidNullabilityOverride") // Remove after b/232460179 is fixed
@Throws(IOException::class, CorruptionException::class)
override suspend fun writeTo(t: Preferences, sink: BufferedSink) {
val preferences = t.asMap()
diff --git a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt b/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
index fa08b90..eed8f1f 100644
--- a/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
+++ b/datastore/datastore-preferences-core/src/jvmTest/kotlin/androidx/datastore/preferences/core/PreferencesSerializerJavaTest.kt
@@ -37,7 +37,7 @@
private val testIO = OkioTestIO()
private lateinit var testFile: OkioPath
- private val preferencesSerializer: OkioSerializer<Preferences> = getPreferencesSerializer()
+ private val preferencesSerializer: OkioSerializer<Preferences> = PreferencesSerializer
private val fileSystem: FileSystem = FileSystem.SYSTEM
@BeforeTest
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
index ed2de2f..7945474 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/Actual.native.kt
@@ -16,7 +16,6 @@
package androidx.datastore.preferences.core
-import androidx.datastore.core.okio.OkioSerializer
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
@@ -42,7 +41,3 @@
// TODO(b/234049307): Pick a better dispatcher for IO
internal actual fun ioDispatcher(): CoroutineDispatcher = Dispatchers.Default
-
-internal actual fun getPreferencesSerializer(): OkioSerializer<Preferences> {
- return PreferencesSerializationSerializer
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
index 74e553f2..7f69d8f 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferenceDataStoreFactory.native.kt
@@ -53,11 +53,11 @@
produceFile: () -> Path
): DataStore<Preferences> {
val delegate = create(
- storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializationSerializer) {
+ storage = OkioStorage(FileSystem.SYSTEM, PreferencesSerializer) {
val file = produceFile()
- check(file.name.endsWith(".${PreferencesSerializationSerializer.fileExtension}")) {
+ check(file.name.endsWith(".${PreferencesSerializer.fileExtension}")) {
"File extension for file: $file does not match required extension for" +
- " Preferences file: ${PreferencesSerializationSerializer.fileExtension}"
+ " Preferences file: ${PreferencesSerializer.fileExtension}"
}
file
},
diff --git a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
similarity index 92%
rename from datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt
rename to datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
index b27e26f..55e28c3 100644
--- a/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializationSerializer.kt
+++ b/datastore/datastore-preferences-core/src/nativeMain/kotlin/androidx/datastore/preferences/core/PreferencesSerializer.native.kt
@@ -18,26 +18,30 @@
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.okio.OkioSerializer
-import okio.BufferedSink
-import okio.BufferedSource
+import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.protobuf.ProtoBuf
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.SerializationException
+import okio.BufferedSink
+import okio.BufferedSource
+/**
+ * Proto based serializer for Preferences. Can be used to manually create
+ * [DataStore][androidx.datastore.core.DataStore] using the
+ * [DataStoreFactory#create][androidx.datastore.core.DataStoreFactory.create] function.
+ */
@OptIn(ExperimentalSerializationApi::class)
-internal object PreferencesSerializationSerializer : OkioSerializer<Preferences> {
- val fileExtension = "preferences_pb"
+actual object PreferencesSerializer : OkioSerializer<Preferences> {
+ internal const val fileExtension = "preferences_pb"
override val defaultValue: Preferences
get() = emptyPreferences()
override suspend fun readFrom(source: BufferedSource): Preferences {
val prefMap: PreferencesMap = try {
- ProtoBuf.decodeFromByteArray<PreferencesMap>(
- source.readByteArray())
+ ProtoBuf.decodeFromByteArray(source.readByteArray())
} catch (e: SerializationException) {
throw CorruptionException("Unable to parse preferences proto.", e)
}
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 6f1bd5f..81afb45 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -23,10 +23,8 @@
# > Configure project :
updated local\.properties
# > Configure project :compose:test\-utils
+# https://youtrack.jetbrains.com/issue/KT-48436
The following Kotlin source sets were configured but not added to any Kotlin compilation:
-\* iosArm[0-9]+Main
-\* iosSimulatorArm[0-9]+Main
-\* iosX[0-9]+Main
\* androidAndroidTestDebug
\* androidAndroidTestRelease
\* androidTestFixtures
@@ -76,13 +74,14 @@
Html results of .* zipped into.*\.zip
# b/230127926
[0-9]+ problem.* found storing the configuration cache.*
-\- Task `:[:A-Za-z0-9#\-]+` of type `org\.jetbrains\.kotlin\.gradle\.plugin\.mpp\.[A-Za-z0-9]+`: invocation of 'Task\.project' at execution time is unsupported\.
-# https://youtrack.jetbrains.com/issue/KT-52694/
-\- Task `:listTaskOutputs` of type `androidx\.build\.ListTaskOutputsTask`: invocation of 'Task\.project' at execution time is unsupported\.
-See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:use_project_during_execution
-# https://youtrack.jetbrains.com/issue/KT-52694
-\- Task \`:[:A-Za-z0-9#\-]+` of type \`org\.jetbrains\.kotlin\.gradle\.tooling\.BuildKotlinToolingMetadataTask\$FromKotlinExtension\`\: invocation of \'Task\.project\' at execution time is unsupported\.
plus [0-9]+ more problems\. Please see the report for details\.
+# https://youtrack.jetbrains.com/issue/KT-43293 fixed in Kotlin 1.8.0
+\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains(\.[a-zA-Z]+)+\.(CInteropCommonizerTask|KotlinNativeCompile|KotlinNativeLink|KotlinNativeHostTest|CInteropMetadataDependencyTransformationTask|GenerateProjectStructureMetadata|TransformKotlinGranularMetadata|NativeDistributionCommonizerTask)\`\: invocation of \'Task\.project\' at execution time is unsupported\.
+\- Task \`((\:[a-z1-9-]+)+)?\:[a-zA-Z1-9]+\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.(CInteropMetadataDependencyTransformationTask|NativeDistributionCommonizerTask)\`: cannot serialize object of type \'org\.gradle(\.[a-zA-Z]+)+\'\, a subtype of \'org\.gradle(\.[a-zA-Z]+)+\'\, as these are not supported with the configuration cache\.
+# https://youtrack.jetbrains.com/issue/KT-54627
+\- Task \`\:commonizeNativeDistribution\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.NativeDistributionCommonizerTask\`\: error writing value of type \'java\.util\.concurrent\.locks\.ReentrantLock\'
+See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:use_project_during_execution
+See https\:\/\/docs\.gradle\.org\/[0-9]+\.[0-9]+\/userguide\/configuration_cache\.html\#config_cache\:requirements\:disallowed_types
See the complete report at file://\$SUPPORT/build/reports/configuration\-cache/[^/]*/[^/]*/configuration\-cache\-report\.html
See the complete report at file://\$OUT_DIR/androidx/build/reports/configuration\-cache/[^ ]*/[^ ]*/configuration\-cache\-report\.html
# > Task :compose:ui:ui:processDebugAndroidTestManifest
diff --git a/development/offlinifyDocs/style.css b/development/offlinifyDocs/style.css
new file mode 100644
index 0000000..c360538
--- /dev/null
+++ b/development/offlinifyDocs/style.css
@@ -0,0 +1 @@
+@charset "UTF-8";body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] .devsite-book-nav-bg:after,body[theme=android-theme][layout=docs]{background-color:#e8eaed}body[theme=android-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-theme] devsite-header .devsite-top-logo-row,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard{background-color:#f7f9fa}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-product-description-row{color:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#202124;fill:#202124}body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:rgba(32,33,36,.7);fill:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-theme] devsite-header .devsite-top-button{color:rgba(32,33,36,.7)}body[theme=android-theme] devsite-header .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#202124}body[theme=android-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#202124}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:#1a73e8}body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:#1967d2}body[theme=android-theme] devsite-header .devsite-header-link,body[theme=android-theme] devsite-header .devsite-header-link:visited,body[theme=android-theme] devsite-header .devsite-settings-kabob,body[theme=android-theme] devsite-header .devsite-settings-kabob:visited{color:#1a73e8}body[theme=android-theme] devsite-header .devsite-header-link:focus,body[theme=android-theme] devsite-header .devsite-header-link:hover,body[theme=android-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-theme] devsite-header .devsite-settings-kabob:hover{color:#1967d2}body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-theme] devsite-header .devsite-header--loading:after,body[theme=android-theme] devsite-header .devsite-header--loading span:after{background-color:#3ddc84}body[theme=android-theme] devsite-toc>.devsite-nav-list{border-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta:not([background]){background-color:#3ddc84;color:#fff}body[theme=android-theme] .devsite-feedback-item-icon-color{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#fff}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-theme] devsite-header .devsite-product-logo-container{color:#3ddc84}body[theme=android-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-theme] devsite-header .devsite-product-logo-container[background]{background:#3ddc84;color:#fff}body[theme=android-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#3ddc84}body[theme=android-theme] .devsite-collapsible-section{background-color:#f7f9fa}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-theme] devsite-tabs.lower-tabs a{color:rgba(32,33,36,.7)}body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#3ddc84}body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#3ddc84}body[theme=android-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#3ddc84;color:#fff}body[theme=android-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-theme] [background=theme]{background-color:#3ddc84}body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-theme] [foreground=theme] .button,body[theme=android-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-theme] [foreground=theme] a:not(.button),body[theme=android-theme] [foreground=theme] a:not(.button) h2,body[theme=android-theme] [foreground=theme] a:not(.button) h3{color:#3ddc84}body[theme=android-theme] [foreground=theme] .button:active,body[theme=android-theme] [foreground=theme] .button:focus,body[theme=android-theme] [foreground=theme] .button:hover{background:#ecfcf3;color:#689f38}body[theme=android-theme] [foreground=theme] .button-primary{background:#3ddc84;color:#fff}body[theme=android-theme] [foreground=theme] .button-primary:active,body[theme=android-theme] [foreground=theme] .button-primary:focus,body[theme=android-theme] [foreground=theme] .button-primary:hover{background:#689f38;color:#fff}body[theme=android-theme] [background=theme] .devsite-landing-row-description,body[theme=android-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-theme] [background=theme] :link:not(.button),body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-theme] [background=theme] :visited:not(.button),body[theme=android-theme] [background=theme] h2,body[theme=android-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#fff}body[theme=android-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}body[theme=android-theme] devsite-content .devsite-404-header h3,body[theme=android-theme] devsite-content .devsite-offline-header h3{color:#689f38}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#f7f9fa}body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-theme] devsite-header .devsite-search-background,body[theme=android-theme] devsite-header .devsite-search-background:after,body[theme=android-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] .devsite-book-nav-bg:after,body[theme=android-ndk-theme][layout=docs]{background-color:#e8eaed}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);-webkit-border-radius:2px;border-radius:2px}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:first-child{-webkit-border-radius:2px 2px 0 0;border-radius:2px 2px 0 0}body[theme=android-ndk-theme][layout=docs] .devsite-landing-row:last-child{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row-wrapper-wrapper:before,body[theme=android-ndk-theme] devsite-header cloudx-tabs-nav.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs .devsite-tabs-wrapper,body[theme=android-ndk-theme] devsite-header devsite-tabs.upper-tabs tab[overflow-tab]:after{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link{color:rgba(0,0,0,.65);fill:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-book-nav .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-guillemet:hover .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:focus .devsite-google-wordmark-svg-path,body[theme=android-ndk-theme] devsite-header .devsite-breadcrumb-link:hover .devsite-google-wordmark-svg-path{fill:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-product-description-row{color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link{color:#fff;fill:#fff}body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-guillemet:hover,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-product-description-row .devsite-breadcrumb-link:hover{color:hsla(0,0%,100%,.7);fill:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:active,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-icon-button:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-top-button{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] devsite-header .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-button:hover{background-color:hsla(0,0%,80%,.15);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-button:active{background-color:hsla(0,0%,80%,.3);color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn{background:transparent;color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:active,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:focus,body[theme=android-ndk-theme] devsite-header .devsite-top-logo-row .devsite-top-button:hover,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:active,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:focus,body[theme=android-ndk-theme] devsite-header devsite-user #devsite-signin-btn:hover{background:transparent;border:0;-webkit-box-shadow:none;box-shadow:none;color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] devsite-header .devsite-header-link,body[theme=android-ndk-theme] devsite-header .devsite-header-link:visited,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:visited{color:rgba(0,0,0,.65)}body[theme=android-ndk-theme] devsite-header .devsite-header-link:focus,body[theme=android-ndk-theme] devsite-header .devsite-header-link:hover,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:focus,body[theme=android-ndk-theme] devsite-header .devsite-settings-kabob:hover{color:rgba(0,0,0,.87)}body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,body[theme=android-ndk-theme] #app-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,body[theme=android-ndk-theme] devsite-header .devsite-header--loading:after,body[theme=android-ndk-theme] devsite-header .devsite-header--loading span:after{background-color:#24c1e0}body[theme=android-ndk-theme] devsite-toc>.devsite-nav-list{border-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta:not([background]){background-color:#24c1e0;color:#202124}body[theme=android-ndk-theme] .devsite-feedback-item-icon-color{background-color:#24c1e0}body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,body[theme=android-ndk-theme] .devsite-landing-row-cta.devsite-landing-row h2{color:#202124}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container,body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container{color:#24c1e0}body[theme=android-ndk-theme] .devsite-search-project .devsite-product-logo-container[background],body[theme=android-ndk-theme] devsite-header .devsite-product-logo-container[background]{background:#24c1e0;color:#fff}body[theme=android-ndk-theme] devsite-header .devsite-product-logo{color:inherit}body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :focus .devsite-landing-row-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] .devsite-landing-row :link .devsite-landing-row-item-icon-container[background][foreground=theme]:hover,body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[background][foreground=theme]:hover{color:#24c1e0}body[theme=android-ndk-theme] .devsite-collapsible-section{background-color:#24c1e0}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a{color:hsla(0,0%,100%,.7)}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:focus,body[theme=android-ndk-theme] devsite-tabs.lower-tabs a:hover,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a{color:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav.lower-tabs tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.lower-tabs tab[active]>a:after{background:#fff}body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] cloudx-tabs-nav tab[dropdown] .devsite-nav-title,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-item-description,body[theme=android-ndk-theme] devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:visited,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav .devsite-tabs-dropdown a:hover,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme=android-ndk-theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab>a{color:#5f6368}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[dropdown] .devsite-tabs-dropdown-content,body[theme=android-ndk-theme] devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:focus,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab>a:hover,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a{color:#202124}body[theme=android-ndk-theme] cloudx-tabs-nav.upper-tabs>div>tab[active]>a:after,body[theme=android-ndk-theme] devsite-tabs.upper-tabs>div>tab[active]>a:after{background:#129eaf}body[theme=android-ndk-theme] devsite-user .devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=android-ndk-theme] devsite-user .devsite-user-dialog .devsite-user-dialog-photo{background:#78d9ec;color:#202124}body[theme=android-ndk-theme] .devsite-landing-row-item-custom-image:not([background]),body[theme=android-ndk-theme] [background=theme]{background-color:#78d9ec}body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h2,body[theme=android-ndk-theme] .devsite-landing-row-item[foreground=theme] :link h3,body[theme=android-ndk-theme] [foreground=theme] .button,body[theme=android-ndk-theme] [foreground=theme] :focus>:not(.material-icons),body[theme=android-ndk-theme] [foreground=theme] :link>:not(.material-icons):hover,body[theme=android-ndk-theme] [foreground=theme] a:not(.button),body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h2,body[theme=android-ndk-theme] [foreground=theme] a:not(.button) h3{color:#24c1e0}body[theme=android-ndk-theme] [foreground=theme] .button:active,body[theme=android-ndk-theme] [foreground=theme] .button:focus,body[theme=android-ndk-theme] [foreground=theme] .button:hover{background:#e9f9fc;color:#129eaf}body[theme=android-ndk-theme] [foreground=theme] .button-primary{background:#24c1e0;color:#202124}body[theme=android-ndk-theme] [foreground=theme] .button-primary:active,body[theme=android-ndk-theme] [foreground=theme] .button-primary:focus,body[theme=android-ndk-theme] [foreground=theme] .button-primary:hover{background:#129eaf;color:#202124}body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-description,body[theme=android-ndk-theme] [background=theme] .devsite-landing-row-item-list-item-icon-container:not([foreground]),body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-description h4+p,body[theme=android-ndk-theme] [background=theme] :link:not(.button),body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) .button-white,body[theme=android-ndk-theme] [background=theme]:not(.devsite-landing-row-cards) h3,body[theme=android-ndk-theme] [background=theme]:not([foreground]):not(.devsite-landing-row-cards),body[theme=android-ndk-theme] [background=theme] :visited:not(.button),body[theme=android-ndk-theme] [background=theme] h2,body[theme=android-ndk-theme] [background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white{color:#202124}body[theme=android-ndk-theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=theme],body[theme=android-ndk-theme] :link .devsite-landing-row-item-list-item-icon-container[foreground=theme]:hover,body[theme=android-ndk-theme] [background=theme] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,body[theme=android-ndk-theme] [background=theme] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}body[theme=android-ndk-theme] devsite-content .devsite-404-header h3,body[theme=android-ndk-theme] devsite-content .devsite-offline-header h3{color:#129eaf}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-searchbox:before{background:#fff}body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-searchbox:before{background:#24c1e0}body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-image,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-image{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field{background:#f1f3f4}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:#e8eaed}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search[search-active] .devsite-search-field:hover{background:#f1f3f4;color:#202124}body[theme=android-ndk-theme] devsite-book-nav .devsite-mobile-header{background:#fff;border-bottom:1px solid #dadce0}@media screen and (max-width:840px){body[theme=android-ndk-theme][layout=docs] .devsite-banner-announcement{-webkit-border-radius:0;border-radius:0}body[theme=android-ndk-theme] devsite-header .devsite-search-background,body[theme=android-ndk-theme] devsite-header .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header[search-active] .devsite-search-background:after,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field,body[theme=android-ndk-theme] devsite-header devsite-search .devsite-search-field:hover{background:0}body[theme=android-ndk-theme][layout=docs] .devsite-article{-webkit-border-radius:0;border-radius:0}}.devsite-site-logo{width:134px}body,dd,div,dl,figure,form,img,input,menu{margin:0;padding:0}body[no-overflow]{overflow:hidden}iframe{border:0}iframe:not([src]){display:none}.caution>:first-child,.dogfood>:first-child,.key-point>:first-child,.key-term>:first-child,.note>:first-child,.objective>:first-child,.quickstart-left>:first-child,.quickstart-wide>:first-child,.special>:first-child,.success>:first-child,.warning>:first-child,aside>:first-child,blockquote>:first-child,dd>:first-child,li>p:first-child,td>.devsite-table-wrapper:first-child>table,td>.expandable:first-child>:nth-child(2),td>:first-child,td>:first-child>li:first-child{margin-top:0}.caution>:last-child,.dogfood>:last-child,.key-point>:last-child,.key-term>:last-child,.note>:last-child,.objective>:last-child,.quickstart-left>:last-child,.quickstart-wide>:last-child,.special>:last-child,.success>:last-child,.warning>:last-child,aside>:last-child,blockquote>:last-child,dd>:last-child,li>p:last-child,td>.devsite-table-wrapper:last-child>table,td>.expandable:last-child>:last-child,td>:last-child,td>:last-child>li:last-child{margin-bottom:0}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.clearfix:after,.quickstart-step:after{clear:both;content:"";display:table;height:0;visibility:hidden}body,html{color:#202124;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;height:100%;margin:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}body[sitemask--active]{overflow:hidden}p{margin:16px 0;padding:0}img,video{border:0;height:auto;max-width:100%}table img{max-width:272px}:link,:visited{color:#1a73e8;outline:0;text-decoration:none}a:focus{text-decoration:underline}a:focus img{-webkit-filter:brightness(75%);filter:brightness(75%)}.devsite-toast-content :link,.devsite-toast-content :visited{color:#fff;text-decoration:underline}.devsite-toast-content a:focus{background:hsla(0,0%,100%,.3);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}sup{line-height:1}dd,ol,ul{margin:0;padding-left:40px}td>dl>dd,td>ol,td>ul{padding-left:20px}ol{list-style:decimal outside}ol ol{list-style-type:lower-alpha}ol ol ol{list-style-type:lower-roman}ol.upper-alpha{list-style-type:upper-alpha}ul{list-style:disc outside}li,li p{margin:12px 0;padding:0}dt{font:700 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}blockquote,dd,dt{margin:16px 0}blockquote{background:#f1f3f4;padding:8px;quotes:none}hr{background:#e8eaed;border:0;height:1px;margin:16px 0;width:100%}abbr,acronym{border-bottom:1px dotted #5f6368;cursor:help}address,cite,dfn,em{font-style:italic}strong{font-weight:700}[visually-hidden]{opacity:0!important;pointer-events:none!important;visibility:hidden!important}.hidden,[hidden]{display:none!important}[render-hidden]{display:inline!important;position:absolute!important;visibility:hidden!important}[no-scroll]{overflow:hidden}#app-progress{left:0;position:fixed;right:0;top:0;z-index:1011}.devsite-article .material-icons{vertical-align:bottom}.devsite-article-body .material-icons:not(:link),[type=landing] .devsite-article .material-icons:not(:link){cursor:default}.footnotes ol{padding-left:16px}.footnotes li{font:400 13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}#qv-wrapper,#table-of-contents,#tb-wrapper,.inline-toc,div.toc:not(.class):not(.group):not(.type):not(.interface),h2#contents,h2.toc,h3#contents,h3.toc,ol.toc,section.toc,ul.toc{display:none}@media screen and (max-width:840px){#app-progress{z-index:1014}}.no-feedback devsite-feedback{display:none!important}.preserve-case{text-transform:none}a.external:after,a[href*=man7\.org]:after,a[href*=oracle\.com]:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"open_in_new";font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.external:after,[dir=ltr] a[href*=man7\.org]:after,[dir=ltr] a[href*=oracle\.com]:after{margin-left:4px}[dir=rtl] a.external:after,[dir=rtl] a[href*=man7\.org]:after,[dir=rtl] a[href*=oracle\.com]:after{margin-right:4px;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}a.download:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"file_download";display:inline-block;font-size:18px;margin:0;vertical-align:text-bottom}[dir=ltr] a.download:before{margin-right:4px}[dir=rtl] a.download:before{margin-left:4px}devsite-content{counter-reset:numbered}h2.numbered{line-height:48px;margin-top:60px;padding-bottom:19px}h2.numbered:before{background:#bdc1c6;-webkit-border-radius:50%;border-radius:50%;color:#fff;content:counter(numbered);counter-increment:numbered;display:inline-block;height:48px;line-height:48px;margin:0 20px 0 0;text-align:center;width:48px}[dir=rtl] h2.numbered:before{margin:0 0 0 20px}.compare-better,.compare-no,.compare-worse,.compare-yes{font-weight:700}.compare-better:before,.compare-blank:before,.compare-no:before,.compare-worse:before,.compare-yes:before{content:"";display:inline-block;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;margin:-4px 4px 0 0;text-transform:none;vertical-align:middle;width:24px}[dir=rtl] .compare-better:before,[dir=rtl] .compare-blank:before,[dir=rtl] .compare-no:before,[dir=rtl] .compare-worse:before,[dir=rtl] .compare-yes:before{margin:-4px 0 0 4px}.compare-better:before{color:#34a853;content:"thumb_up"}.compare-no:before{color:#dd2c00;content:"not_interested"}.compare-worse:before{color:#dd2c00;content:"thumb_down"}.compare-yes:before{color:#34a853;content:"check"}.align-center{text-align:center}.align-right{text-align:right}.hanging-indent,.members.function td:first-child{padding-left:25px;text-indent:-17px}[dir=rtl] .hanging-indent,[dir=rtl] .members.function td:first-child{padding-left:0;padding-right:25px}.bad-table{table-layout:fixed}.bad-table td,.bad-table tr{word-wrap:break-word}.bad-table pre{word-wrap:normal}.screenshot{border:1px solid #e8eaed;padding:3px}.columns td,.columns th,.columns tr{background:0;border:0;font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}[dir=ltr] .columns td,[dir=ltr] .columns th{padding-right:20px}[dir=rtl] .columns td,[dir=rtl] .columns th{padding-left:20px}.columns th{color:#202124;font-weight:500}.columns code,.columns pre{background:#f1f3f4}.inline:not(.expandable){display:inline}.inline-block{display:inline-block}.block{display:block}img.inline-icon{height:1.2em;vertical-align:sub}.no-select{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{max-width:-webkit-calc((100% - 40px)/2);max-width:calc((100% - 40px)/2)}.attempt-left,.video-wrapper-left,aside.attempt-left{float:left;margin:0 40px 40px 0}.attempt-right,.video-wrapper,[dir=rtl] .attempt-left,[dir=rtl] .video-wrapper-left,[dir=rtl] aside.attempt-left,aside.attempt-right{float:right;margin:0 0 40px 40px}[dir=rtl] .attempt-right,[dir=rtl] .video-wrapper,[dir=rtl] aside.attempt-right{float:left;margin:0 40px 40px 0}.attempt-left+.attempt-right,.attempt-left+.video-wrapper,.video-wrapper-left+.attempt-right,.video-wrapper-left+.video-wrapper,[dir=rtl] .attempt-left+.attempt-right,[dir=rtl] .attempt-left+.video-wrapper,[dir=rtl] .video-wrapper-left+.attempt-right,[dir=rtl] .video-wrapper-left+.video-wrapper{margin:0 0 40px}.video-wrapper,.video-wrapper-full-width{overflow:hidden;position:relative}.video-wrapper,.video-wrapper-left{width:-webkit-calc((100% - 40px)/2);width:calc((100% - 40px)/2)}.video-wrapper-full-width{margin:16px 0;width:100%}.video-wrapper-full-width embed,.video-wrapper-full-width iframe,.video-wrapper-full-width object,.video-wrapper-left embed,.video-wrapper-left iframe,.video-wrapper-left object,.video-wrapper embed,.video-wrapper iframe,.video-wrapper object{height:101%;left:-.5%;position:absolute;top:-.5%;width:101%}@media screen and (max-width:840px){.attempt-left,.attempt-right,aside.attempt-left,aside.attempt-right{display:block;max-width:100%}.attempt-left,.attempt-right,.video-wrapper,.video-wrapper-left,aside.attempt-left,aside.attempt-right{float:none;margin:16px 0;width:100%}}body[pending] #gc-wrapper{margin-top:0!important}body[ready] #gc-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-wrapper{min-height:100vh}body[ready] .devsite-wrapper{min-height:100%;overflow:hidden}@supports ((display:-webkit-flex) or (display:flex)){body[ready] .devsite-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.full-bleed{margin:0;padding:0}.devsite-book-nav-bg,devsite-book-nav{width:268px}body[pending] devsite-book-nav{position:absolute}@media screen and (max-width:840px){body[devsite-book-nav--open]{overflow:hidden}body[devsite-book-nav--open] devsite-book-nav[fixed]{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}}body devsite-toc.devsite-toc{-ms-grid-column:5;grid-column:3;-ms-grid-row:1;grid-row:1;margin:24px 24px 0 0;min-width:0;width:auto}[dir=rtl] body devsite-toc.devsite-toc{margin:24px 0 0 24px}body devsite-toc>.devsite-nav-list{width:auto}.devsite-main-content{margin:0 auto;position:relative;width:100%;z-index:1003}#contain-402{z-index:1004!important}body[pending] .devsite-main-content{min-height:-webkit-calc(100vh - 456px);min-height:calc(100vh - 456px)}body[ready] .devsite-main-content{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:auto}body[layout=full] .devsite-main-content{max-width:1600px;padding:40px 80px}body[layout=full][type=error] .devsite-main-content{max-width:1600px;padding:0 80px}body[layout=full][type=landing] .devsite-main-content{max-width:none;padding:0}body[layout=docs] .devsite-main-content{display:-ms-grid;display:grid;grid-gap:24px;-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr;-ms-grid-rows:1fr;grid-template-rows:1fr}body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:1fr minmax(752px,936px) minmax(160px,1fr)}body[layout=docs] .devsite-main-content[has-book-nav]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px 1fr;grid-template-columns:minmax(268px,1fr) minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:minmax(268px,1fr) 24px minmax(752px,936px) 24px minmax(160px,1fr);grid-template-columns:minmax(268px,1fr) minmax(752px,936px) minmax(160px,1fr)}.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 0 0 268px}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin:0 268px 0 0}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~.devsite-footer,.devsite-main-content[has-book-nav]~devsite-footer-promos,.devsite-main-content[has-book-nav]~devsite-footer-utility{margin-left:0}[dir=rtl] .devsite-main-content[has-book-nav]~.devsite-footer,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos,[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility{margin-right:0}}@media screen and (max-width:1252px){body[layout=docs] .devsite-main-content[has-toc]{-ms-grid-columns:1fr 24px minmax(752px,936px) 24px 1fr;grid-template-columns:1fr minmax(752px,936px) 1fr}body[layout=docs] .devsite-main-content[has-book-nav],body[layout=docs] .devsite-main-content[has-book-nav][has-toc]{-ms-grid-columns:268px 24px 1fr 24px;grid-template-columns:268px 1fr 0}}@media screen and (max-width:840px){body[layout=full] .devsite-main-content{padding:24px}body[layout=full][type=error] .devsite-main-content{padding:0 24px}body[layout=docs] .devsite-main-content{display:block;min-width:100%}devsite-content-footer{padding:0 24px}}@media screen and (max-width:600px){body[layout=full] .devsite-main-content{padding:16px}body[layout=full][type=error] .devsite-main-content,devsite-content-footer{padding:0 16px}}.devsite-icon:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-icon-arrow-drop-down:before{content:"arrow_drop_down"}.devsite-icon-code:before{content:"code"}.devsite-icon-code-dark:before,.devsite-icon-code-light:before{content:"brightness_medium"}.devsite-icon-copy:before{content:"content_copy"}aside{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] aside{padding-left:60px}[dir=rtl] aside{padding-right:60px}body[layout=full]:not([type=landing]) aside{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) aside{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) aside{padding-right:calc(50vw - 50% + 36px)}aside:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] aside:before{float:left;margin-left:-36px}[dir=rtl] aside:before{float:right;margin-right:-36px}aside :link,aside :visited{text-decoration:underline}aside a:focus,aside a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}aside pre{background:hsla(0,0%,100%,.75)}aside code{font-weight:700;padding:0}.caution{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .caution{padding-left:60px}[dir=rtl] .caution{padding-right:60px}body[layout=full]:not([type=landing]) .caution{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .caution{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .caution{padding-right:calc(50vw - 50% + 36px)}.caution:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .caution:before{float:left;margin-left:-36px}[dir=rtl] .caution:before{float:right;margin-right:-36px}.caution :link,.caution :visited{text-decoration:underline}.caution a:focus,.caution a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.caution pre{background:hsla(0,0%,100%,.75)}.caution code{font-weight:700;padding:0}.dogfood{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .dogfood{padding-left:60px}[dir=rtl] .dogfood{padding-right:60px}body[layout=full]:not([type=landing]) .dogfood{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .dogfood{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .dogfood{padding-right:calc(50vw - 50% + 36px)}.dogfood:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .dogfood:before{float:left;margin-left:-36px}[dir=rtl] .dogfood:before{float:right;margin-right:-36px}.dogfood :link,.dogfood :visited{text-decoration:underline}.dogfood a:focus,.dogfood a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.dogfood pre{background:hsla(0,0%,100%,.75)}.dogfood code{font-weight:700;padding:0}.key-point{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-point{padding-left:60px}[dir=rtl] .key-point{padding-right:60px}body[layout=full]:not([type=landing]) .key-point{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-point{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-point{padding-right:calc(50vw - 50% + 36px)}.key-point:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-point:before{float:left;margin-left:-36px}[dir=rtl] .key-point:before{float:right;margin-right:-36px}.key-point :link,.key-point :visited{text-decoration:underline}.key-point a:focus,.key-point a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-point pre{background:hsla(0,0%,100%,.75)}.key-point code{font-weight:700;padding:0}.key-term{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .key-term{padding-left:60px}[dir=rtl] .key-term{padding-right:60px}body[layout=full]:not([type=landing]) .key-term{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .key-term{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .key-term{padding-right:calc(50vw - 50% + 36px)}.key-term:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .key-term:before{float:left;margin-left:-36px}[dir=rtl] .key-term:before{float:right;margin-right:-36px}.key-term :link,.key-term :visited{text-decoration:underline}.key-term a:focus,.key-term a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.key-term pre{background:hsla(0,0%,100%,.75)}.key-term code{font-weight:700;padding:0}.note{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .note{padding-left:60px}[dir=rtl] .note{padding-right:60px}body[layout=full]:not([type=landing]) .note{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .note{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .note{padding-right:calc(50vw - 50% + 36px)}.note:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .note:before{float:left;margin-left:-36px}[dir=rtl] .note:before{float:right;margin-right:-36px}.note :link,.note :visited{text-decoration:underline}.note a:focus,.note a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.note pre{background:hsla(0,0%,100%,.75)}.note code{font-weight:700;padding:0}.objective{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .objective{padding-left:60px}[dir=rtl] .objective{padding-right:60px}body[layout=full]:not([type=landing]) .objective{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .objective{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .objective{padding-right:calc(50vw - 50% + 36px)}.objective:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .objective:before{float:left;margin-left:-36px}[dir=rtl] .objective:before{float:right;margin-right:-36px}.objective :link,.objective :visited{text-decoration:underline}.objective a:focus,.objective a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.objective pre{background:hsla(0,0%,100%,.75)}.objective code{font-weight:700;padding:0}.special{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .special{padding-left:60px}[dir=rtl] .special{padding-right:60px}body[layout=full]:not([type=landing]) .special{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .special{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .special{padding-right:calc(50vw - 50% + 36px)}.special:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .special:before{float:left;margin-left:-36px}[dir=rtl] .special:before{float:right;margin-right:-36px}.special :link,.special :visited{text-decoration:underline}.special a:focus,.special a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.special pre{background:hsla(0,0%,100%,.75)}.special code{font-weight:700;padding:0}.success{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .success{padding-left:60px}[dir=rtl] .success{padding-right:60px}body[layout=full]:not([type=landing]) .success{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .success{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .success{padding-right:calc(50vw - 50% + 36px)}.success:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .success:before{float:left;margin-left:-36px}[dir=rtl] .success:before{float:right;margin-right:-36px}.success :link,.success :visited{text-decoration:underline}.success a:focus,.success a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.success pre{background:hsla(0,0%,100%,.75)}.success code{font-weight:700;padding:0}.warning{display:block;font-size:14px;margin:16px 0;padding:16px 24px}[dir=ltr] .warning{padding-left:60px}[dir=rtl] .warning{padding-right:60px}body[layout=full]:not([type=landing]) .warning{margin:16px calc(50% - 50vw);padding:16px calc(50vw - 50%)}[dir=ltr] body[layout=full]:not([type=landing]) .warning{padding-left:calc(50vw - 50% + 36px)}[dir=rtl] body[layout=full]:not([type=landing]) .warning{padding-right:calc(50vw - 50% + 36px)}.warning:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin:0}[dir=ltr] .warning:before{float:left;margin-left:-36px}[dir=rtl] .warning:before{float:right;margin-right:-36px}.warning :link,.warning :visited{text-decoration:underline}.warning a:focus,.warning a:hover{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.warning pre{background:hsla(0,0%,100%,.75)}.warning code{font-weight:700;padding:0}@media screen and (max-width:600px){body[layout=full] aside{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] aside{padding-left:76px}[dir=rtl] body[layout=full] aside{padding-right:76px}body[layout=full] .caution{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .caution{padding-left:76px}[dir=rtl] body[layout=full] .caution{padding-right:76px}body[layout=full] .dogfood{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .dogfood{padding-left:76px}[dir=rtl] body[layout=full] .dogfood{padding-right:76px}body[layout=full] .key-point{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-point{padding-left:76px}[dir=rtl] body[layout=full] .key-point{padding-right:76px}body[layout=full] .key-term{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .key-term{padding-left:76px}[dir=rtl] body[layout=full] .key-term{padding-right:76px}body[layout=full] .note{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .note{padding-left:76px}[dir=rtl] body[layout=full] .note{padding-right:76px}body[layout=full] .objective{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .objective{padding-left:76px}[dir=rtl] body[layout=full] .objective{padding-right:76px}body[layout=full] .special{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .special{padding-left:76px}[dir=rtl] body[layout=full] .special{padding-right:76px}body[layout=full] .success{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .success{padding-left:76px}[dir=rtl] body[layout=full] .success{padding-right:76px}body[layout=full] .warning{margin:16px -40px;padding:16px 40px}[dir=ltr] body[layout=full] .warning{padding-left:76px}[dir=rtl] body[layout=full] .warning{padding-right:76px}}aside var{background:inherit;font-weight:700;padding:0}.note,.note :link,.note :visited,.note code,.special,.special :link,.special :visited,.special code,aside,aside :link,aside :visited,aside code{background:#e1f5fe;color:#01579b}.note:before,.special:before,aside:before{content:"star"}.caution,.caution :link,.caution :visited,.caution code{background:#feefe3;color:#bf360c}.caution:before{content:"error"}.dogfood,.dogfood :link,.dogfood :visited,.dogfood code{background:#eceff1;color:#546e7a}.dogfood:before{content:"pets"}.key-point,.key-point :link,.key-point :visited,.key-point code{background:#e8eaf6;color:#3f51b5}.key-point:before{content:"lightbulb_outline"}.key-term,.key-term :link,.key-term :visited,.key-term code{background:#f3e8fd;color:#9334e6}.key-term:before{content:"font_download"}.objective,.objective :link,.objective :visited,.objective code,.success,.success :link,.success :visited,.success code{background:#e0f2f1;color:#00796b}.objective:before{content:"school"}.success:before{content:"check_circle"}.warning,.warning :link,.warning :visited,.warning code{background:#fce8e6;color:#d50000}.warning:before{content:"warning"}.caution :focus code,.caution :hover code,.dogfood :focus code,.dogfood :hover code,.key-point :focus code,.key-point :hover code,.key-term :focus code,.key-term :hover code,.note :focus code,.note :hover code,.objective :focus code,.objective :hover code,.special :focus code,.special :hover code,.success :focus code,.success :hover code,.warning :focus code,.warning :hover code,aside :focus code,aside :hover code{background:transparent}.devsite-no-page-title>.caution:first-child,.devsite-no-page-title>.dogfood:first-child,.devsite-no-page-title>.key-point:first-child,.devsite-no-page-title>.key-term:first-child,.devsite-no-page-title>.note:first-child,.devsite-no-page-title>.objective:first-child,.devsite-no-page-title>.special:first-child,.devsite-no-page-title>.success:first-child,.devsite-no-page-title>.warning:first-child,.devsite-no-page-title>aside:first-child{clear:right}.devsite-banner{font-size:14px}.devsite-banner :link,.devsite-banner :visited{text-decoration:underline}body[layout=full] .devsite-banner{margin:-40px calc(50% - 50vw) 40px}body[type=landing][layout] .devsite-banner{margin:0}body[layout=docs] .devsite-banner{margin:-40px -40px 40px}.devsite-banner-message{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0 auto;max-width:1520px;padding:20px 40px}body[layout=full] .devsite-banner-message,body[type=landing] .devsite-banner-message,body[type=landing][layout=docs] .devsite-banner-message{padding:20px 40px}[dir=ltr] .devsite-banner-message-text{margin-right:auto}[dir=rtl] .devsite-banner-message-text{margin-left:auto}.devsite-banner[background] a:not(.button):focus,.devsite-banner a:not(.button):focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}.devsite-banner[background=white]{border-bottom:1px solid #dadce0}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#e1f5fe}.devsite-banner-announcement[background] :link,.devsite-banner-announcement[background] :visited{background:0}.devsite-banner-confidential{background:#feefe3;color:#bf360c}.devsite-banner-confidential .devsite-banner-message:before{content:"warning";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}[dir=ltr] .devsite-banner-confidential .devsite-banner-message:before{margin-right:16px}[dir=rtl] .devsite-banner-confidential .devsite-banner-message:before{margin-left:16px}.devsite-banner-translated{background:#f1f3f4;color:rgba(0,0,0,.65)}.devsite-banner-translated :link{text-decoration:none}.devsite-banner .button,.devsite-banner button{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:-6px 0;text-decoration:none}[dir=ltr] .devsite-banner .button,[dir=ltr] .devsite-banner button{margin-left:16px}[dir=rtl] .devsite-banner .button,[dir=rtl] .devsite-banner button{margin-right:16px}.devsite-banner .material-icons{margin:-2px 0;vertical-align:middle}.devsite-banner-translated-image{margin:4px 0 -4px;width:122px}[dir=ltr] .devsite-banner-translated-image{margin-right:24px}[dir=rtl] .devsite-banner-translated-image{margin-left:24px}.devsite-banner-heading{font-weight:700}@media screen and (max-width:1252px){.devsite-banner-translated .devsite-banner-translated-text{display:block}}@media screen and (max-width:840px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-24px -24px 24px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{padding:20px 24px}body[layout=full] .devsite-banner{margin-bottom:40px}}@media screen and (max-width:600px){body[layout=docs] .devsite-banner,body[layout=full] .devsite-banner{margin:-16px -16px 16px}body[layout=full] .devsite-banner{margin-bottom:40px}.devsite-banner-message,body[layout] .devsite-banner-message,body[layout][type] .devsite-banner-message,body[type] .devsite-banner-message{display:block;padding:16px}[dir] .devsite-banner .button,[dir] .devsite-banner button{margin:12px 0 0}}.devsite-card-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 0 -24px}[dir=rtl] .devsite-card-group{margin:-24px -24px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 33.333%;flex:0 0 33.333%;min-width:0;padding:24px 0 0 24px}[dir=rtl] .devsite-card-wrapper{padding:24px 24px 0 0}.devsite-card-wrapper[hidden]{display:none}.devsite-card-list-link,.devsite-card h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}.devsite-card h3{margin:0 0 10px}.devsite-card{background:#fff;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;height:100%;position:relative}.devsite-card-image-bg{background-position:50%;background-repeat:no-repeat;-o-background-size:cover;background-size:cover;padding:0 0 56.25%}.devsite-card-content-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-card-content{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;padding:16px;word-break:break-word}.devsite-card-category{font:700 12px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;margin-top:0;text-transform:uppercase}.devsite-card-summary{-webkit-box-orient:vertical;-webkit-line-clamp:4;display:-webkit-box;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 0;max-height:96px;overflow:hidden}.devsite-card-author{border-top:1px solid #dadce0;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;font-size:12px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;line-height:16px;min-height:40px;position:relative}.devsite-card-author,.devsite-card-buttons{padding:16px}.devsite-card-author-date,.devsite-card-author-name{margin:0}.devsite-card-author-name+.devsite-card-author-date{margin-top:8px}.devsite-card-author+.devsite-card-buttons,.devsite-card-content+.devsite-card-buttons{padding-top:0}.devsite-card-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;margin:auto 0 0}.devsite-card-list{list-style:none;padding:0}.devsite-card-list-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.devsite-card-list-item:not(:last-child){margin:0 0 20px;padding:0}.devsite-card-list-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#1a73e8;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;padding:16px 20px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}.devsite-card-list-link:focus,.devsite-card-list-link:hover{background:#e4eefc}.devsite-card-list-link:focus{text-decoration:none}.devsite-card-list-link:after{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;margin-left:auto;opacity:0;-webkit-transform:translateX(-20px);-o-transform:translateX(-20px);transform:translateX(-20px);-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}.devsite-card-list-link:focus:after,.devsite-card-list-link:hover:after{opacity:1;-webkit-transform:translateX(0);-o-transform:translateX(0);transform:translateX(0)}@media screen and (max-width:840px){.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){.devsite-card-group{margin:-16px 0 0 -16px}[dir=rtl] .devsite-card-group{margin:-16px -16px 0 0}.devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}[dir=rtl] .devsite-card-wrapper{padding:16px 16px 0 0}.devsite-card-summary{font-size:14px;line-height:20px;max-height:80px}}.pre-style,code,pre{background:#f1f3f4;color:#37474f;font:400 100%/1 Roboto Mono,monospace;padding:1px 4px}code{font:500 90%/1 Roboto Mono,monospace;word-break:break-word}.pre-style code,pre code,table code{font-weight:400;word-break:normal}.pre-style,pre{font:14px/20px Roboto Mono,monospace;margin:16px 0;overflow-x:auto;padding:24px;position:relative}.pre-style code,pre code{background:0;font-size:14px;padding:0}b code,strong code{font-weight:700}pre.devsite-code-highlight>span{opacity:.54}td>pre:only-child{padding:0}td>devsite-code:only-child pre,td>devsite-code pre.inline-code{padding:0 64px 0 0}td>devsite-code:not([dark-code]):only-child pre,td>devsite-code pre.inline-code{background:0}td>devsite-code:only-child pre~.devsite-code-buttons-container,td>devsite-code pre.inline-code~.devsite-code-buttons-container{top:-6px}h1 code,h2 code,h3 code,h4 code,h5 code,h6 code{background:0;color:#212121;padding:0}h1 code{color:#757575}a code,td a code{color:#1967d2}body[layout] .devsite-main-content var span,var,var code{color:#ec407a;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-weight:700}pre.clear-for-copy{all:initial;left:-99999px;position:absolute;top:-99999px;white-space:pre}pre.clear-for-copy *{all:unset;font-family:Roboto Mono,monospace;white-space:pre}fieldset{border:0;margin:0;padding:0}label{color:#5f6368;display:block;font-size:12px}input+label{color:#202124;display:inline;font-size:16px}label[for]{cursor:pointer}input[type=checkbox],input[type=radio]{-webkit-appearance:none;background:#fff;-webkit-border-radius:2px;border-radius:2px;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:18px;margin:-2px 8px 2px 0;outline:0;position:relative;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;vertical-align:middle;width:18px}[dir=rtl] input[type=checkbox],[dir=rtl] input[type=radio]{margin:-2px 0 2px 8px}input[type=checkbox]{color:#5f6368}input[type=radio]{-webkit-border-radius:50%;border-radius:50%;color:#5f6368;-webkit-transition:none;-o-transition:none;transition:none}input[type=checkbox]:focus:before,input[type=radio]:focus:before{background:#e8eaed}input[type=checkbox]:checked,input[type=checkbox]:indeterminate,input[type=radio]:checked{color:#1a73e8}input[type=checkbox]:checked:focus:before,input[type=checkbox]:indeterminate:focus:before,input[type=radio]:focus:before{background:#d2e3fc}input[type=checkbox]:after,input[type=radio]:after{content:"check_box_outline_blank";position:relative;right:3px;top:-3px;z-index:1}[dir=rtl] input[type=checkbox]:after,[dir=rtl] input[type=radio]:after{left:3px;right:auto}input[type=checkbox]:checked:after{content:"check_box"}input[type=checkbox]:indeterminate:after{content:"indeterminate_check_box"}input[type=radio]:after{content:"radio_button_unchecked"}input[type=radio]:checked:after{content:"radio_button_checked"}input[type=checkbox]:before,input[type=radio]:before{-webkit-border-radius:50%;border-radius:50%;content:"";display:block;height:36px;left:-9px;position:absolute;top:-9px;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;width:36px}[dir=rtl] input[type=checkbox]:before,[dir=rtl] input[type=radio]:before{left:auto;right:-9px}input:disabled+label,input[type=checkbox]:disabled,input[type=radio]:disabled{color:#bdc1c6;cursor:default}input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select[multiple],select[size],textarea{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;color:#202124;font:16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;max-width:100%;outline:0;padding:7px;-webkit-transition:border-color .2s;-o-transition:border-color .2s;transition:border-color .2s;vertical-align:middle}input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus,select[multiple]:focus,select[size]:focus,textarea:focus{border-bottom:2px solid #1a73e8;padding-bottom:6px}input[type=date]:disabled,input[type=datetime-local]:disabled,input[type=datetime]:disabled,input[type=email]:disabled,input[type=month]:disabled,input[type=number]:disabled,input[type=password]:disabled,input[type=search]:disabled,input[type=tel]:disabled,input[type=text]:disabled,input[type=time]:disabled,input[type=url]:disabled,input[type=week]:disabled,select[multiple]:disabled,select[size]:disabled,textarea:disabled{background:#f1f3f4}body input[type=file]{height:auto;line-height:1;padding:8px 16px}select{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}select:focus,select:hover{background-color:#f1f3f4}select:active{background-color:#e8eaed}select:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}select::-ms-expand{display:none}devsite-book-nav .devsite-breadcrumb-list,devsite-content .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:0;white-space:nowrap}.devsite-search-project .devsite-breadcrumb-list,devsite-book-nav .devsite-breadcrumb-list,devsite-header .devsite-breadcrumb-list{display:-webkit-box;display:-webkit-flex;display:flex}devsite-content .devsite-breadcrumb-list{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-flex-wrap:wrap;flex-wrap:wrap;font-size:13px}body[layout=full] devsite-content .devsite-breadcrumb-list,body[type=landing] devsite-content .devsite-breadcrumb-list{display:none}devsite-book-nav .devsite-breadcrumb-item,devsite-content .devsite-breadcrumb-item,devsite-header .devsite-breadcrumb-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-book-nav .devsite-breadcrumb-guillemet,devsite-header .devsite-breadcrumb-guillemet{font-size:24px;margin:0 4px;width:24px}devsite-content .devsite-breadcrumb-guillemet{color:#5f6368;font-size:18px;margin:0 4px;width:18px}devsite-book-nav .devsite-breadcrumb-guillemet:before,devsite-content .devsite-breadcrumb-guillemet:before,devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_right"}[dir=rtl] devsite-book-nav .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-content .devsite-breadcrumb-guillemet:before,[dir=rtl] devsite-header .devsite-breadcrumb-guillemet:before{content:"chevron_left"}devsite-book-nav .devsite-breadcrumb-link,devsite-content .devsite-breadcrumb-link,devsite-header .devsite-breadcrumb-link{display:inline-block;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-book-nav .devsite-breadcrumb-link:focus,devsite-book-nav .devsite-breadcrumb-link:hover,devsite-header .devsite-breadcrumb-link:focus,devsite-header .devsite-breadcrumb-link:hover{text-decoration:none}devsite-content .devsite-breadcrumb-link{color:#5f6368}devsite-content .devsite-breadcrumb-link:focus,devsite-content .devsite-breadcrumb-link:hover{color:#1a73e8;text-decoration:none}.devsite-nav{font-size:13px}.devsite-nav-list,.devsite-nav-responsive-tabs,.devsite-nav-section{list-style-type:none;padding:0}.devsite-nav-item{line-height:16px;margin:0}.devsite-nav-title{color:#202124;display:-webkit-box;display:-webkit-flex;display:flex;padding:4px 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.devsite-nav-title[href]:focus,.devsite-nav-title[href]:hover{color:#1a73e8;text-decoration:none}.devsite-nav-heading>.devsite-nav-title{color:rgba(0,0,0,.65);font-weight:700}.devsite-nav-active{font-weight:500}.devsite-nav-active,.devsite-nav-active.devsite-nav-title,.devsite-nav-active.devsite-nav-title>.devsite-nav-icon:before,.devsite-nav-deprecated .devsite-nav-active.devsite-nav-title{color:#1a73e8}.devsite-nav-text{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-nav-text>span{pointer-events:none}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title-no-path:focus,.devsite-nav-title-no-path:focus{color:#1a73e8}.devsite-nav-icon{cursor:default;font-size:18px;margin:-1px 0 -1px 4px}[dir=rtl] .devsite-nav-icon{margin:-1px 4px -1px 0}.devsite-nav-icon:before{color:#5f6368;content:"info"}.devsite-nav-icon[data-icon=alpha]:before,.devsite-nav-icon[data-icon=beta]:before,.devsite-nav-icon[data-icon=experimental]:before{content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 18 18'><path d='M15.78,13.39L11,7V4h2V2H5v2h2v3l-4.9,6.53c-0.34,0.47-0.39,1.1-0.12,1.62C2.24,15.67,2.77,16,3.36,16h11.28 c0.86,0,1.56-0.7,1.56-1.56C16.2,14.04,16.03,13.67,15.78,13.39z' fill='%2380868b'/></svg>")}.devsite-nav-deprecated.devsite-nav-accordion .devsite-nav-title,.devsite-nav-deprecated .devsite-nav-title,.devsite-nav-icon[data-icon=deprecated]:before{color:#bdc1c6}.devsite-nav-icon[data-icon=deprecated]:before{content:"not_interested"}.devsite-nav-icon[data-icon=external]:before{content:"open_in_new"}[dir=rtl] .devsite-nav-icon[data-icon=external]:before{display:inline-block;-webkit-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.devsite-nav-icon[data-icon=forward]:before{content:"arrow_forward";cursor:pointer}[dir=rtl] .devsite-nav-icon[data-icon=forward]:before{content:"arrow_back"}.devsite-nav-icon[data-icon=limited]:before{content:"verified_user"}.devsite-nav-icon[data-icon=new]:before{content:"new_releases"}.devsite-nav-icon[data-icon=nightly]:before{content:"nights_stay"}.button,.devsite-footer-utility-button>a,button,input[type=button],input[type=file],input[type=image],input[type=reset],input[type=submit]{-moz-appearance:none;-webkit-appearance:none;background:#fff;-webkit-box-sizing:border-box;box-sizing:border-box;color:#1a73e8;cursor:pointer;display:inline-block;height:36px;margin:0;min-width:36px;outline:0;overflow:hidden;text-decoration:none;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s,border .2s,-webkit-box-shadow .2s;transition:background-color .2s,border .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s;transition:background-color .2s,border .2s,box-shadow .2s,-webkit-box-shadow .2s;vertical-align:middle;white-space:nowrap;border:0;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 16px;text-transform:uppercase}.button:focus,.button:hover,.devsite-footer-utility-button>a:focus,.devsite-footer-utility-button>a:hover,button:focus,button:hover,input[type=button]:focus,input[type=button]:hover,input[type=file]:focus,input[type=file]:hover,input[type=image]:focus,input[type=image]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=submit]:focus,input[type=submit]:hover{background:#e4eefc}.button:active,.devsite-footer-utility-button>a:active,button:active,input[type=button]:active,input[type=file]:active,input[type=image]:active,input[type=reset]:active,input[type=submit]:active{background:#c8ddf9;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button.button-disabled,.button.button-disabled:active,.button.button-disabled:focus,.button.button-disabled:hover,[foreground] .button.button-disabled,[foreground] .button.button-disabled:active,[foreground] .button.button-disabled:focus,[foreground] .button.button-disabled:hover,body[theme] [foreground] .button.button-disabled,body[theme] [foreground] .button.button-disabled:active,body[theme] [foreground] .button.button-disabled:focus,body[theme] [foreground] .button.button-disabled:hover,button[disabled],button[disabled]:active,button[disabled]:focus,button[disabled]:hover,input[type=button][disabled],input[type=button][disabled]:active,input[type=button][disabled]:focus,input[type=button][disabled]:hover,input[type=file][disabled],input[type=file][disabled]:active,input[type=file][disabled]:focus,input[type=file][disabled]:hover,input[type=image][disabled],input[type=image][disabled]:active,input[type=image][disabled]:focus,input[type=image][disabled]:hover,input[type=reset][disabled],input[type=reset][disabled]:active,input[type=reset][disabled]:focus,input[type=reset][disabled]:hover,input[type=submit][disabled],input[type=submit][disabled]:active,input[type=submit][disabled]:focus,input[type=submit][disabled]:hover{-webkit-box-shadow:none;box-shadow:none;cursor:default;pointer-events:none;background:#ddd;color:rgba(0,0,0,.26)}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{background:#1a73e8;color:#fff}.button-blue:focus,.button-blue:hover,.button-green:focus,.button-green:hover,.button-primary:focus,.button-primary:hover,.button-red:focus,.button-red:hover,body devsite-footer-utility .devsite-footer-utility-button>a:focus,body devsite-footer-utility .devsite-footer-utility-button>a:hover{background:#1765cc;color:#fff}.button-blue:active,.button-green:active,.button-primary:active,.button-red:active,body devsite-footer-utility .devsite-footer-utility-button>a:active{background:#0277bd;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary{background:#fff;color:#1a73e8}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:focus,[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:hover{background:#e4eefc}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-primary:active{background:#c8ddf9}.button-white{background:0;color:#1a73e8;padding:0 8px}.button-white,.button-white:active,.button-white:focus,.button-white:hover{border:0;-webkit-box-shadow:none;box-shadow:none}.button-white.button-disabled,.button-white[disabled]{background:0}.button-raised{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}.button-raised:focus,.button-raised:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.button-raised:active{-webkit-box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15);box-shadow:0 1px 3px 0 rgba(60,64,67,.3),0 4px 8px 3px rgba(60,64,67,.15)}.button+.button,button+button,input[type=button]+input[type=button],input[type=file]+input[type=file],input[type=image]+input[type=image],input[type=reset]+input[type=reset],input[type=submit]+input[type=submit]{margin-left:16px}[dir=rtl] .button+.button,[dir=rtl] button+button,[dir=rtl] input[type=button]+input[type=button],[dir=rtl] input[type=file]+input[type=file],[dir=rtl] input[type=image]+input[type=image],[dir=rtl] input[type=reset]+input[type=reset],[dir=rtl] input[type=submit]+input[type=submit]{margin-left:0;margin-right:16px}.button-flat+.button-flat,.button-white+.button-white,button+.button{margin-left:8px}[dir=rtl] .button-flat+.button-flat,[dir=rtl] .button-white+.button-white,[dir=rtl] button+.button{margin-left:0;margin-right:8px}.button:focus{text-decoration:none}.button-flat{padding:0 8px}.button-flat,.button-flat:active,.button-flat:focus,.button-flat:hover{background:0;border:0;-webkit-box-shadow:none;box-shadow:none}.button-flat:disabled{background-color:transparent}.button-transparent{padding:0 8px}.button-transparent,.button-transparent:focus,.button-transparent:hover{background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.button-text-white{color:#fff}.button-text-blue{color:#1a73e8}.button-lowercase{text-transform:none}.button-unindented{margin-left:-8px}[dir=rtl] .button-unindented{margin-left:0;margin-right:-8px}.button-icon,.button>.material-icons,button>.material-icons{font-size:18px;height:18px;width:18px}.button.button-with-icon,.button.external{padding:0 16px}.button>.material-icons,button>.material-icons{margin:0 8px;position:relative;top:-2px;vertical-align:middle}.button>.button-icon,button>.button-icon{margin:0 8px}.button-with-icon>.button-icon,.button-with-icon>.material-icons{margin:0 8px 0 -4px}.button-with-icon>.icon-after,.button.external:not(.button-with-icon):after,[dir=rtl] .button-with-icon>.button-icon,[dir=rtl] .button-with-icon>.material-icons,button.external:not(.button-with-icon):after{margin:0 -4px 0 8px}[dir=rtl] .button-with-icon>.icon-after,[dir=rtl] .button.external:not(.button-with-icon):after,[dir=rtl] button.external:not(.button-with-icon):after{margin:0 8px 0 -4px}.button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:-4px}.button:not(.button-with-icon)>.icon-after,[dir=rtl] .button:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:-4px}[dir=rtl] .button:not(.button-with-icon)>.icon-after{margin-left:-4px}.button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-left:4px}.button-white:not(.button-with-icon)>.icon-after,[dir=rtl] .button-white:not(.button-with-icon)>.material-icons:not(.icon-after){margin-right:4px}[dir=rtl] .button-white:not(.button-with-icon)>.icon-after{margin-left:4px}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:hover{background:rgba(154,160,166,.3)}[background]:not([background=grey]):not(.devsite-landing-row-cards) .button-white:focus{background:rgba(154,160,166,.5)}.devsite-landing-row-item[foreground=grey] .button,[foreground=grey] .button{background:#5f6368}.devsite-landing-row-item[foreground=grey] .button:active,.devsite-landing-row-item[foreground=grey] .button:focus,.devsite-landing-row-item[foreground=grey] .button:hover,[foreground=grey] .button:active,[foreground=grey] .button:focus,[foreground=grey] .button:hover{background:#3c4043}devsite-header .button,devsite-header .button:active,devsite-header .button:focus,devsite-header .button:hover{-webkit-box-shadow:none;box-shadow:none}h1,h2,h3,h4,h5,h6{outline:0}[layout=docs] h1,[layout=docs] h2,[layout=docs] h3,[layout=docs] h4,[layout=docs] h5,[layout=docs] h6{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}.devsite-article h1:first-of-type{margin-top:0;position:relative;top:-4px}.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{color:#5f6368;font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}.devsite-landing-row .devsite-catalog-alphabet-letter-heading h2{margin:20px 0}[layout=docs] h2{border-bottom:1px solid #e8eaed;padding-bottom:3px}.devsite-landing-row h3,h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em;margin:40px 0 20px}h3{margin:32px 0 16px}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4,h3{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row-item-no-media h3,.devsite-landing-row h4{margin:32px 0 12px;padding:0}.devsite-landing-row-large-headings .devsite-landing-row-item-list h4{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:-.01em}h4,h5,h6{margin:32px 0 16px}h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h5{font:700 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h6{font:500 14px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}h1+dl>dt:first-child,h1+h1,h1+h2,h1+h3,h1+h4,h1+h5,h1+h6,h2+dl>dt:first-child,h2+h1,h2+h2,h2+h3,h2+h4,h2+h5,h2+h6,h3+dl>dt:first-child,h3+h1,h3+h2,h3+h3,h3+h4,h3+h5,h3+h6,h4+dl>dt:first-child,h4+h1,h4+h2,h4+h3,h4+h4,h4+h5,h4+h6,h5+dl>dt:first-child,h5+h1,h5+h2,h5+h3,h5+h4,h5+h5,h5+h6,h6+dl>dt:first-child,h6+h1,h6+h2,h6+h3,h6+h4,h6+h5,h6+h6{margin-top:0}@media screen and (max-width:600px){.devsite-landing-row-large-headings .devsite-landing-row-item-description h3,.devsite-landing-row h2,h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}table{border:0;border-collapse:collapse;border-spacing:0;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:16px 0 15px;width:100%}caption{background:#f1f3f4;font-weight:500;padding:12px 8px;text-align:center}tr{border:0;border-bottom:1px solid #dadce0}tr:first-child{border-top:1px solid #dadce0}td,th{border:0;margin:0;text-align:left}[dir=rtl] td,[dir=rtl] th{text-align:right}th{height:48px;padding:8px;vertical-align:middle}th>devsite-heading>h2,th>devsite-heading>h3,th>h2,th>h3{border:0;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0!important;padding:0!important}td>.expandable>h2.showalways,td>.expandable>h3.showalways,td>h2:only-child,td>h3:only-child{border:0;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding-bottom:0}td>.expandable>h2.showalways,td>.expandable>h3.showalways{line-height:24px}td b,td strong,th b,th strong{font-weight:500}td,td code{padding:7px 8px 8px}td code,th code{background:0;font:500 100%/1 Roboto Mono,monospace;-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;padding:0}td pre code{color:#37474f;font-weight:400;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}table.responsive td,table.responsive th{min-width:214px}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{min-width:120px}table.responsive td code,table.responsive th code{word-break:break-all;word-break:break-word}table.responsive td tr:not(.alt) td:first-child,table.responsive tr:not(.alt) td td:first-child,td{background:hsla(0,0%,100%,.95);vertical-align:top}.devsite-table-wrapper{margin:16px 0;overflow:auto}.devsite-table-wrapper .devsite-table-wrapper{margin:0;overflow:visible}.devsite-table-wrapper table{margin:0}.devsite-table-wrapper .devsite-table-wrapper table{margin:16px 0}table.responsive table.responsive{margin:0}table.responsive td tr:first-child td{padding-top:0}table.responsive td tr:last-child td{padding-bottom:0}[dir=ltr] table.responsive td td:first-child{padding-left:0}[dir=rtl] table.responsive td td:first-child{padding-right:0}table.responsive>*>tr>th:not(:first-child),table.responsive>tr>th:not(:first-child){display:none}table.columns tr{border:0}table table tr:first-child{border-top:0}devsite-selector .devsite-table-wrapper:last-child tr:last-child,table table tr:last-child{border-bottom:0}th,th code{background:#e8eaed;color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}table.vertical-rules tr>td:not(:last-child),table.vertical-rules tr>th:not(:last-child){border-right:1px solid #dadce0}.alt td,td.alt{background:rgba(241,243,244,.75)}table.responsive>*>tr:not(.alt) td:first-child,table.responsive>tr:not(.alt) td:first-child{background:hsla(0,0%,96.5%,.87)}table.blue tr{background:#e8eaf6;border-bottom-color:#c5cae9}table.cyan tr{background:#e4f7fb;border-bottom-color:#a1e4f2}table.green tr{background:#e0f2f1;border-bottom-color:#b2dfdb}table.orange tr{background:#feefe3;border-bottom-color:#fedfc8}table.pink tr{background:#fde7f3;border-bottom-color:#fdcfe8}table.purple tr{background:#f3e8fd;border-bottom-color:#e9d2fd}table.blue tr:first-child{border-top-color:#c5cae9}table.cyan tr:first-child{border-top-color:#a1e4f2}table.green tr:first-child{border-top-color:#b2dfdb}table.orange tr:first-child{border-top-color:#fedfc8}table.pink tr:first-child{border-top-color:#fdcfe8}table.purple tr:first-child{border-top-color:#e9d2fd}table.blue th,table.cyan th,table.green th,table.orange th,table.pink th,table.purple th{background:inherit}table.blue tr.alt td,table.responsive.blue tr:not(.alt) td:first-child{background:#f6f7fb}table.responsive.blue table td:first-child{background:#fff}table.cyan tr.alt td,table.responsive.cyan tr:not(.alt) td:first-child{background:#f4fcfd}table.responsive.cyan table td:first-child{background:#fff}table.green tr.alt td,table.responsive.green tr:not(.alt) td:first-child{background:#f3faf9}table.responsive.green table td:first-child{background:#fff}table.orange tr.alt td,table.responsive.orange tr:not(.alt) td:first-child{background:#fff9f4}table.responsive.orange table td:first-child{background:#fff}table.pink tr.alt td,table.responsive.pink tr:not(.alt) td:first-child{background:#fef5fa}table.responsive.pink table td:first-child{background:#fff}table.purple tr.alt td,table.responsive.purple tr:not(.alt) td:first-child{background:#faf6fe}table.responsive.purple table td:first-child{background:#fff}table.vertical-rules.blue tr>td:not(:last-child),table.vertical-rules.blue tr>th:not(:last-child){border-right:1px solid #c5cae9}table.vertical-rules.cyan tr>td:not(:last-child),table.vertical-rules.cyan tr>th:not(:last-child){border-right:1px solid #a1e4f2}table.vertical-rules.green tr>td:not(:last-child),table.vertical-rules.green tr>th:not(:last-child){border-right:1px solid #b2dfdb}table.vertical-rules.orange tr>td:not(:last-child),table.vertical-rules.orange tr>th:not(:last-child){border-right:1px solid #fedfc8}table.vertical-rules.pink tr>td:not(:last-child),table.vertical-rules.pink tr>th:not(:last-child){border-right:1px solid #fdcfe8}table.vertical-rules.purple tr>td:not(:last-child),table.vertical-rules.purple tr>th:not(:last-child){border-right:1px solid #e9d2fd}.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table,.devsite-article-body>table.full-width{margin:16px -16px}}.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:first-child,.devsite-article-body>.devsite-full-width-table th:first-child,.devsite-article-body>table.full-width td:first-child,.devsite-article-body>table.full-width th:first-child{padding-left:16px}}.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:40px}@media screen and (max-width:840px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:24px}}@media screen and (max-width:600px){.devsite-article-body>.devsite-full-width-table td:last-child,.devsite-article-body>.devsite-full-width-table th:last-child,.devsite-article-body>table.full-width td:last-child,.devsite-article-body>table.full-width th:last-child{padding-right:16px}}.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-left:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:first-child,.devsite-full-width-table table table th:first-child{padding-right:0}}.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-right:0}@media screen and (max-width:840px){.devsite-full-width-table table table td:last-child,.devsite-full-width-table table table th:last-child{padding-left:0}}@media screen and (max-width:840px){table.responsive td,table.responsive th,table.responsive tr{display:block}table.responsive table:not(.responsive) tr{display:table-row}table.responsive table:not(.responsive) td,table.responsive table:not(.responsive) th{display:table-cell}table.responsive>*>th,table.responsive>th{height:auto;padding:14px 8px}}.devsite-book-nav::-webkit-scrollbar,.devsite-dialog::-webkit-scrollbar,.devsite-popout::-webkit-scrollbar,.devsite-table-wrapper::-webkit-scrollbar,.devsite-tabs-overflow-menu::-webkit-scrollbar,.devsite-toc::-webkit-scrollbar,[scrollbars]::-webkit-scrollbar,pre::-webkit-scrollbar{height:8px;width:8px}.devsite-book-nav::-webkit-scrollbar-thumb,.devsite-dialog::-webkit-scrollbar-thumb,.devsite-popout::-webkit-scrollbar-thumb,.devsite-table-wrapper::-webkit-scrollbar-thumb,.devsite-tabs-overflow-menu::-webkit-scrollbar-thumb,.devsite-toc::-webkit-scrollbar-thumb,[scrollbars]::-webkit-scrollbar-thumb,pre::-webkit-scrollbar-thumb{background:rgba(128,134,139,.26);-webkit-border-radius:8px;border-radius:8px}.devsite-doc-set-nav-row::-webkit-scrollbar,.devsite-header-upper-tabs::-webkit-scrollbar,[no-horizontal-scrollbars]::-webkit-scrollbar{height:0;width:0}.devsite-table-wrapper::-webkit-scrollbar-corner,[scrollbars]::-webkit-scrollbar-corner,pre::-webkit-scrollbar-corner{background:0}.devsite-cse-confidential-results{background:rgba(254,239,227,.5);margin:16px -40px;padding:0 40px 16px}.devsite-cse-confidential-results+aside{margin-top:-16px!important}.devsite-search-results-stats{margin-bottom:8px}.devsite-search-results-restricted .gs-title{font-weight:500}.devsite-search-results-restricted .gs-title:link,.devsite-search-results-restricted .gs-title:visited{color:#039be5}.devsite-search-results-restricted .gs-visibleUrl{color:#1e8e3e;font-size:14px}.devsite-result-item-link .devsite-result-item-confidential,.devsite-search-results-restricted .gs-title-label{background:#feefe3;-webkit-border-radius:4px;border-radius:4px;color:#bf360c;display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:0 8px;padding:5px 8px 3px;text-transform:uppercase}body[type=search] .gsc-webResult .gsc-result{border:none;margin:24px 0;padding:0}.devsite-search-page-controls{margin-top:8px}.devsite-search-project{border-bottom:1px solid #dadce0;margin-bottom:24px;padding-bottom:23px}.devsite-search-project .devsite-project-scoped-results-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-bottom:8px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:focus,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-link:hover,.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo{height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}.devsite-search-project .devsite-project-scoped-results-title .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo-link canvas{height:auto!important}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-right:4px}[dir=rtl] .devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container{margin-left:4px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}.devsite-search-project .devsite-project-scoped-results-title .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}.devsite-search-project .devsite-project-scoped-results-title .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-item,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-product-name{width:100%}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}.devsite-search-project .devsite-project-scoped-results-title .devsite-product-name-wrapper{position:relative;margin-left:.3em}.devsite-search-project .devsite-breadcrumb-list,.devsite-search-project .devsite-project-scoped-results-title{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-search-project .devsite-breadcrumb-link,.devsite-search-project .devsite-breadcrumb-link:hover{color:#202124}.devsite-search-project .devsite-breadcrumb-link .devsite-google-wordmark{fill:currentColor}.devsite-search-title{margin:0;padding:0}.devsite-search-title .devsite-search-term{color:#202124;font-weight:500}.devsite-steps{padding:24px 0 40px;-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-steps,.devsite-steps .steps-direction{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-steps .steps-direction{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;font-weight:500;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative}.devsite-steps .steps-previous{margin-left:40px}.devsite-steps .steps-next{margin-right:40px;text-align:right}.devsite-steps .steps-link:focus{text-decoration:none}.devsite-steps .steps-link-direction{color:#1a73e8;display:block;font-size:14px}.devsite-steps .steps-link-title{color:#01579b;font-size:18px}.devsite-steps .steps-link:focus .steps-link-title{border-bottom:1px solid #01579b}.devsite-steps .steps-link-arrow{color:#1a73e8;position:absolute}.devsite-steps .steps-link-arrow-left{left:-40px}.devsite-steps .steps-link-arrow-right{right:-40px}@media screen and (max-width:840px){.devsite-steps .steps-link-title{font-size:14px}.devsite-steps .steps-previous{margin-left:24px}.devsite-steps .steps-next{margin-right:24px}.devsite-steps .steps-link-arrow-left{left:-24px}.devsite-steps .steps-link-arrow-right{right:-24px}.devsite-steps .steps-link-arrow{bottom:4px;font-size:16px}}@media screen and (max-width:600px){.devsite-steps{padding:8px 0 16px}.devsite-steps .steps-next,.devsite-steps .steps-previous{margin:0}.devsite-steps .steps-link-arrow{display:none}}.devsite-jsfiddle-hide,devsite-googler-buttons,devsite-playlist,devsite-quiz,devsite-search .devsite-popout,html[cached] .devsite-wrapper{display:none}.devsite-dialog:not([is-upgraded]),iframe.devsite-embedded-youtube-video:not([is-upgraded]){pointer-events:none;visibility:hidden}.code-sample,.data-sample,.ds-selector-dropdown,.ds-selector-tabs,.expandable,.kd-tabbed-horz,.kd-tabbed-vert{display:none}devsite-selector{pointer-events:none;visibility:hidden}devsite-search .devsite-searchbox{background:#f1f3f4;-webkit-border-radius:2px;border-radius:2px}devsite-page-rating[position=header]{-webkit-box-flex:0;-webkit-flex:0 0 120px;flex:0 0 120px;margin:0 0 0 16px;width:120px}[dir=rtl] devsite-page-rating[position=header]{margin:0 16px 0 0}google-codelab{display:none}iframe.framebox,iframe.inherit-locale{display:block;width:100%}[background]:not(.devsite-landing-row-cards),[background]:not(.devsite-landing-row-cards) h3,[background] h2{color:#fff}[background=grey]{background-color:#f1f3f4}[background=grey] h2{color:#5f6368}[background=grey]:not(.devsite-landing-row-cards),[background=grey]:not(.devsite-landing-row-cards) [background] h3,[background=grey]:not(.devsite-landing-row-cards) h3{color:inherit}[background] .devsite-landing-row-description{color:#fff}[background=grey] .devsite-landing-row-description{color:#202124}[background] :link:not(.button),[background] :visited:not(.button){color:#fff}[background=grey] :link:not(.button),[background=grey] :visited:not(.button),[background].devsite-landing-row-cards :link:not(.button),[background].devsite-landing-row-cards :visited:not(.button){color:#1a73e8}[background]:not([background=grey]) :focus>:not(.material-icons),[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:underline}[background]:not([background=grey]) p>a:focus{background:hsla(0,0%,100%,.7);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px;text-decoration:none}[foreground] :focus>:not(.material-icons),[foreground] :link>:not(.material-icons):hover{text-decoration:underline}[foreground=blue-grey] .button{color:#607d8b}[foreground=blue-grey] .button:active,[foreground=blue-grey] .button:focus,[foreground=blue-grey] .button:hover{background:#eff2f3}[foreground=blue-grey] .button-primary{background:#607d8b;color:#fff}[foreground=blue-grey] .button-primary:active,[foreground=blue-grey] .button-primary:focus,[foreground=blue-grey] .button-primary:hover{background:#455a64}[background=blue-grey]{background-color:#607d8b}[foreground=blue-grey] :focus>:not(.material-icons),[foreground=blue-grey] :link>:not(.material-icons):hover,[foreground=blue-grey] a:not(.button) h2,[foreground=blue-grey] a:not(.button) h3{color:#607d8b}.devsite-landing-row[background=blue-grey]+.devsite-landing-row[background=blue-grey]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-icon-container[foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey],.devsite-landing-row-item[foreground=blue-grey] :link h2,.devsite-landing-row-item[foreground=blue-grey] :link h3{color:#607d8b}.devsite-landing-row-item-icon-container[background=blue-grey],.devsite-landing-row-item-list-item-icon-container[background=blue-grey]{background:#607d8b}[foreground=blue-grey-dark] .button{color:#455a64}[foreground=blue-grey-dark] .button:active,[foreground=blue-grey-dark] .button:focus,[foreground=blue-grey-dark] .button:hover{background:#eceff0}[foreground=blue-grey-dark] .button-primary{background:#455a64;color:#fff}[foreground=blue-grey-dark] .button-primary:active,[foreground=blue-grey-dark] .button-primary:focus,[foreground=blue-grey-dark] .button-primary:hover{background:#37474f}[background=blue-grey-dark]{background-color:#455a64}[foreground=blue-grey-dark] :focus>:not(.material-icons),[foreground=blue-grey-dark] :link>:not(.material-icons):hover,[foreground=blue-grey-dark] a:not(.button) h2,[foreground=blue-grey-dark] a:not(.button) h3{color:#455a64}.devsite-landing-row[background=blue-grey-dark]+.devsite-landing-row[background=blue-grey-dark]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background][foreground=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[foreground=blue-grey-dark],.devsite-landing-row-item[foreground=blue-grey-dark] :link h2,.devsite-landing-row-item[foreground=blue-grey-dark] :link h3{color:#455a64}.devsite-landing-row-item-icon-container[background=blue-grey-dark],.devsite-landing-row-item-list-item-icon-container[background=blue-grey-dark]{background:#455a64}[foreground=deep-orange] .button{color:#ff5722}[foreground=deep-orange] .button:active,[foreground=deep-orange] .button:focus,[foreground=deep-orange] .button:hover{background:#ffeee9}[foreground=deep-orange] .button-primary{background:#ff5722;color:#fff}[foreground=deep-orange] .button-primary:active,[foreground=deep-orange] .button-primary:focus,[foreground=deep-orange] .button-primary:hover{background:#e64a19}[background=deep-orange]{background-color:#ff5722}[foreground=deep-orange] :focus>:not(.material-icons),[foreground=deep-orange] :link>:not(.material-icons):hover,[foreground=deep-orange] a:not(.button) h2,[foreground=deep-orange] a:not(.button) h3{color:#ff5722}.devsite-landing-row[background=deep-orange]+.devsite-landing-row[background=deep-orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-icon-container[foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-orange],.devsite-landing-row-item-list-item-icon-container[foreground=deep-orange],.devsite-landing-row-item[foreground=deep-orange] :link h2,.devsite-landing-row-item[foreground=deep-orange] :link h3{color:#ff5722}.devsite-landing-row-item-icon-container[background=deep-orange],.devsite-landing-row-item-list-item-icon-container[background=deep-orange]{background:#ff5722}[foreground=deep-purple] .button{color:#673ab7}[foreground=deep-purple] .button:active,[foreground=deep-purple] .button:focus,[foreground=deep-purple] .button:hover{background:#f0ebf8}[foreground=deep-purple] .button-primary{background:#673ab7;color:#fff}[foreground=deep-purple] .button-primary:active,[foreground=deep-purple] .button-primary:focus,[foreground=deep-purple] .button-primary:hover{background:#512da8}[background=deep-purple]{background-color:#673ab7}[foreground=deep-purple] :focus>:not(.material-icons),[foreground=deep-purple] :link>:not(.material-icons):hover,[foreground=deep-purple] a:not(.button) h2,[foreground=deep-purple] a:not(.button) h3{color:#673ab7}.devsite-landing-row[background=deep-purple]+.devsite-landing-row[background=deep-purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-icon-container[foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=deep-purple],.devsite-landing-row-item-list-item-icon-container[foreground=deep-purple],.devsite-landing-row-item[foreground=deep-purple] :link h2,.devsite-landing-row-item[foreground=deep-purple] :link h3{color:#673ab7}.devsite-landing-row-item-icon-container[background=deep-purple],.devsite-landing-row-item-list-item-icon-container[background=deep-purple]{background:#673ab7}[foreground=google-blue] .button{color:#1a73e8}[foreground=google-blue] .button:active,[foreground=google-blue] .button:focus,[foreground=google-blue] .button:hover{background:#e8f1fd}[foreground=google-blue] .button-primary{background:#1a73e8;color:#fff}[foreground=google-blue] .button-primary:active,[foreground=google-blue] .button-primary:focus,[foreground=google-blue] .button-primary:hover{background:#185abc}[background=google-blue]{background-color:#1a73e8}[foreground=google-blue] :focus>:not(.material-icons),[foreground=google-blue] :link>:not(.material-icons):hover,[foreground=google-blue] a:not(.button) h2,[foreground=google-blue] a:not(.button) h3{color:#1a73e8}.devsite-landing-row[background=google-blue]+.devsite-landing-row[background=google-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-icon-container[foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-blue],.devsite-landing-row-item-list-item-icon-container[foreground=google-blue],.devsite-landing-row-item[foreground=google-blue] :link h2,.devsite-landing-row-item[foreground=google-blue] :link h3{color:#1a73e8}.devsite-landing-row-item-icon-container[background=google-blue],.devsite-landing-row-item-list-item-icon-container[background=google-blue]{background:#1a73e8}[foreground=google-green] .button{color:#1e8e3e}[foreground=google-green] .button:active,[foreground=google-green] .button:focus,[foreground=google-green] .button:hover{background:#e9f4ec}[foreground=google-green] .button-primary{background:#1e8e3e;color:#fff}[foreground=google-green] .button-primary:active,[foreground=google-green] .button-primary:focus,[foreground=google-green] .button-primary:hover{background:#137333}[background=google-green]{background-color:#1e8e3e}[foreground=google-green] :focus>:not(.material-icons),[foreground=google-green] :link>:not(.material-icons):hover,[foreground=google-green] a:not(.button) h2,[foreground=google-green] a:not(.button) h3{color:#1e8e3e}.devsite-landing-row[background=google-green]+.devsite-landing-row[background=google-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-icon-container[foreground=google-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-green],.devsite-landing-row-item-list-item-icon-container[foreground=google-green],.devsite-landing-row-item[foreground=google-green] :link h2,.devsite-landing-row-item[foreground=google-green] :link h3{color:#1e8e3e}.devsite-landing-row-item-icon-container[background=google-green],.devsite-landing-row-item-list-item-icon-container[background=google-green]{background:#1e8e3e}[foreground=google-red] .button{color:#d93025}[foreground=google-red] .button:active,[foreground=google-red] .button:focus,[foreground=google-red] .button:hover{background:#fbeae9}[foreground=google-red] .button-primary{background:#d93025;color:#fff}[foreground=google-red] .button-primary:active,[foreground=google-red] .button-primary:focus,[foreground=google-red] .button-primary:hover{background:#b31412}[background=google-red]{background-color:#d93025}[foreground=google-red] :focus>:not(.material-icons),[foreground=google-red] :link>:not(.material-icons):hover,[foreground=google-red] a:not(.button) h2,[foreground=google-red] a:not(.button) h3{color:#d93025}.devsite-landing-row[background=google-red]+.devsite-landing-row[background=google-red]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-icon-container[foreground=google-red],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-red],.devsite-landing-row-item-list-item-icon-container[foreground=google-red],.devsite-landing-row-item[foreground=google-red] :link h2,.devsite-landing-row-item[foreground=google-red] :link h3{color:#d93025}.devsite-landing-row-item-icon-container[background=google-red],.devsite-landing-row-item-list-item-icon-container[background=google-red]{background:#d93025}[foreground=indigo] .button{color:#3f51b5}[foreground=indigo] .button:active,[foreground=indigo] .button:focus,[foreground=indigo] .button:hover{background:#eceef8}[foreground=indigo] .button-primary{background:#3f51b5;color:#fff}[foreground=indigo] .button-primary:active,[foreground=indigo] .button-primary:focus,[foreground=indigo] .button-primary:hover{background:#303f9f}[background=indigo]{background-color:#3f51b5}[foreground=indigo] :focus>:not(.material-icons),[foreground=indigo] :link>:not(.material-icons):hover,[foreground=indigo] a:not(.button) h2,[foreground=indigo] a:not(.button) h3{color:#3f51b5}.devsite-landing-row[background=indigo]+.devsite-landing-row[background=indigo]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-icon-container[foreground=indigo],.devsite-landing-row-item-list-item-icon-container[background][foreground=indigo],.devsite-landing-row-item-list-item-icon-container[foreground=indigo],.devsite-landing-row-item[foreground=indigo] :link h2,.devsite-landing-row-item[foreground=indigo] :link h3{color:#3f51b5}.devsite-landing-row-item-icon-container[background=indigo],.devsite-landing-row-item-list-item-icon-container[background=indigo]{background:#3f51b5}[foreground=light-blue] .button{color:#0288d1}[foreground=light-blue] .button:active,[foreground=light-blue] .button:focus,[foreground=light-blue] .button:hover{background:#e6f3fa}[foreground=light-blue] .button-primary{background:#0288d1;color:#fff}[foreground=light-blue] .button-primary:active,[foreground=light-blue] .button-primary:focus,[foreground=light-blue] .button-primary:hover{background:#01579b}[background=light-blue]{background-color:#0288d1}[foreground=light-blue] :focus>:not(.material-icons),[foreground=light-blue] :link>:not(.material-icons):hover,[foreground=light-blue] a:not(.button) h2,[foreground=light-blue] a:not(.button) h3{color:#0288d1}.devsite-landing-row[background=light-blue]+.devsite-landing-row[background=light-blue]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-icon-container[foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-blue],.devsite-landing-row-item-list-item-icon-container[foreground=light-blue],.devsite-landing-row-item[foreground=light-blue] :link h2,.devsite-landing-row-item[foreground=light-blue] :link h3{color:#0288d1}.devsite-landing-row-item-icon-container[background=light-blue],.devsite-landing-row-item-list-item-icon-container[background=light-blue]{background:#0288d1}[foreground=nest-theme] .button{color:#00afd8}[foreground=nest-theme] .button:active,[foreground=nest-theme] .button:focus,[foreground=nest-theme] .button:hover{background:#e6f7fb}[foreground=nest-theme] .button-primary{background:#00afd8;color:#fff}[foreground=nest-theme] .button-primary:active,[foreground=nest-theme] .button-primary:focus,[foreground=nest-theme] .button-primary:hover{background:#0096c8}[background=nest-theme]{background-color:#00afd8}[foreground=nest-theme] :focus>:not(.material-icons),[foreground=nest-theme] :link>:not(.material-icons):hover,[foreground=nest-theme] a:not(.button) h2,[foreground=nest-theme] a:not(.button) h3{color:#00afd8}.devsite-landing-row[background=nest-theme]+.devsite-landing-row[background=nest-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-icon-container[foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=nest-theme],.devsite-landing-row-item-list-item-icon-container[foreground=nest-theme],.devsite-landing-row-item[foreground=nest-theme] :link h2,.devsite-landing-row-item[foreground=nest-theme] :link h3{color:#00afd8}.devsite-landing-row-item-icon-container[background=nest-theme],.devsite-landing-row-item-list-item-icon-container[background=nest-theme]{background:#00afd8}[foreground=pink] .button{color:#e52592}[foreground=pink] .button:active,[foreground=pink] .button:focus,[foreground=pink] .button:hover{background:#fce9f4}[foreground=pink] .button-primary{background:#e52592;color:#fff}[foreground=pink] .button-primary:active,[foreground=pink] .button-primary:focus,[foreground=pink] .button-primary:hover{background:#b80672}[background=pink]{background-color:#e52592}[foreground=pink] :focus>:not(.material-icons),[foreground=pink] :link>:not(.material-icons):hover,[foreground=pink] a:not(.button) h2,[foreground=pink] a:not(.button) h3{color:#e52592}.devsite-landing-row[background=pink]+.devsite-landing-row[background=pink]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=pink],.devsite-landing-row-item-icon-container[foreground=pink],.devsite-landing-row-item-list-item-icon-container[background][foreground=pink],.devsite-landing-row-item-list-item-icon-container[foreground=pink],.devsite-landing-row-item[foreground=pink] :link h2,.devsite-landing-row-item[foreground=pink] :link h3{color:#e52592}.devsite-landing-row-item-icon-container[background=pink],.devsite-landing-row-item-list-item-icon-container[background=pink]{background:#e52592}[foreground=purple] .button{color:#9334e6}[foreground=purple] .button:active,[foreground=purple] .button:focus,[foreground=purple] .button:hover{background:#f4ebfd}[foreground=purple] .button-primary{background:#9334e6;color:#fff}[foreground=purple] .button-primary:active,[foreground=purple] .button-primary:focus,[foreground=purple] .button-primary:hover{background:#7627bb}[background=purple]{background-color:#9334e6}[foreground=purple] :focus>:not(.material-icons),[foreground=purple] :link>:not(.material-icons):hover,[foreground=purple] a:not(.button) h2,[foreground=purple] a:not(.button) h3{color:#9334e6}.devsite-landing-row[background=purple]+.devsite-landing-row[background=purple]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=purple],.devsite-landing-row-item-icon-container[foreground=purple],.devsite-landing-row-item-list-item-icon-container[background][foreground=purple],.devsite-landing-row-item-list-item-icon-container[foreground=purple],.devsite-landing-row-item[foreground=purple] :link h2,.devsite-landing-row-item[foreground=purple] :link h3{color:#9334e6}.devsite-landing-row-item-icon-container[background=purple],.devsite-landing-row-item-list-item-icon-container[background=purple]{background:#9334e6}[foreground=stadia-theme] .button{color:#9b0063}[foreground=stadia-theme] .button:active,[foreground=stadia-theme] .button:focus,[foreground=stadia-theme] .button:hover{background:#f5e6ef}[foreground=stadia-theme] .button-primary{background:#9b0063;color:#fff}[foreground=stadia-theme] .button-primary:active,[foreground=stadia-theme] .button-primary:focus,[foreground=stadia-theme] .button-primary:hover{background:#680039}[background=stadia-theme]{background-color:#9b0063}[foreground=stadia-theme] :focus>:not(.material-icons),[foreground=stadia-theme] :link>:not(.material-icons):hover,[foreground=stadia-theme] a:not(.button) h2,[foreground=stadia-theme] a:not(.button) h3{color:#9b0063}.devsite-landing-row[background=stadia-theme]+.devsite-landing-row[background=stadia-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=stadia-theme],.devsite-landing-row-item-list-item-icon-container[foreground=stadia-theme],.devsite-landing-row-item[foreground=stadia-theme] :link h2,.devsite-landing-row-item[foreground=stadia-theme] :link h3{color:#9b0063}.devsite-landing-row-item-icon-container[background=stadia-theme],.devsite-landing-row-item-list-item-icon-container[background=stadia-theme]{background:#9b0063}[foreground=teal] .button{color:#009688}[foreground=teal] .button:active,[foreground=teal] .button:focus,[foreground=teal] .button:hover{background:#e6f5f3}[foreground=teal] .button-primary{background:#009688;color:#fff}[foreground=teal] .button-primary:active,[foreground=teal] .button-primary:focus,[foreground=teal] .button-primary:hover{background:#00796b}[background=teal]{background-color:#009688}[foreground=teal] :focus>:not(.material-icons),[foreground=teal] :link>:not(.material-icons):hover,[foreground=teal] a:not(.button) h2,[foreground=teal] a:not(.button) h3{color:#009688}.devsite-landing-row[background=teal]+.devsite-landing-row[background=teal]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=teal],.devsite-landing-row-item-icon-container[foreground=teal],.devsite-landing-row-item-list-item-icon-container[background][foreground=teal],.devsite-landing-row-item-list-item-icon-container[foreground=teal],.devsite-landing-row-item[foreground=teal] :link h2,.devsite-landing-row-item[foreground=teal] :link h3{color:#009688}.devsite-landing-row-item-icon-container[background=teal],.devsite-landing-row-item-list-item-icon-container[background=teal]{background:#009688}[foreground=youtube-theme] .button{color:red}[foreground=youtube-theme] .button:active,[foreground=youtube-theme] .button:focus,[foreground=youtube-theme] .button:hover{background:#ffe6e6}[foreground=youtube-theme] .button-primary{background:red;color:#fff}[foreground=youtube-theme] .button-primary:active,[foreground=youtube-theme] .button-primary:focus,[foreground=youtube-theme] .button-primary:hover{background:#c20000}[background=youtube-theme]{background-color:red}[foreground=youtube-theme] :focus>:not(.material-icons),[foreground=youtube-theme] :link>:not(.material-icons):hover,[foreground=youtube-theme] a:not(.button) h2,[foreground=youtube-theme] a:not(.button) h3{color:red}.devsite-landing-row[background=youtube-theme]+.devsite-landing-row[background=youtube-theme]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background][foreground=youtube-theme],.devsite-landing-row-item-list-item-icon-container[foreground=youtube-theme],.devsite-landing-row-item[foreground=youtube-theme] :link h2,.devsite-landing-row-item[foreground=youtube-theme] :link h3{color:red}.devsite-landing-row-item-icon-container[background=youtube-theme],.devsite-landing-row-item-list-item-icon-container[background=youtube-theme]{background:red}[foreground=cyan] .button{color:#12b5cb}[foreground=cyan] .button:active,[foreground=cyan] .button:focus,[foreground=cyan] .button:hover{background:#e7f8fa}[foreground=cyan] .button-primary{background:#12b5cb;color:#202124}[foreground=cyan] .button-primary:active,[foreground=cyan] .button-primary:focus,[foreground=cyan] .button-primary:hover{background:#098591}[background=cyan]{background-color:#43cde6}[foreground=cyan] :focus>:not(.material-icons),[foreground=cyan] :link>:not(.material-icons):hover,[foreground=cyan] a:not(.button) h2,[foreground=cyan] a:not(.button) h3{color:#12b5cb}.devsite-landing-row[background=cyan]+.devsite-landing-row[background=cyan]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-icon-container[foreground=cyan],.devsite-landing-row-item-list-item-icon-container[background][foreground=cyan],.devsite-landing-row-item-list-item-icon-container[foreground=cyan],.devsite-landing-row-item[foreground=cyan] :link h2,.devsite-landing-row-item[foreground=cyan] :link h3{color:#12b5cb}.devsite-landing-row-item-icon-container[background=cyan],.devsite-landing-row-item-list-item-icon-container[background=cyan]{background:#43cde6}[foreground=google-yellow] .button{color:#f9ab00}[foreground=google-yellow] .button:active,[foreground=google-yellow] .button:focus,[foreground=google-yellow] .button:hover{background:#fef7e6}[foreground=google-yellow] .button-primary{background:#f9ab00;color:#202124}[foreground=google-yellow] .button-primary:active,[foreground=google-yellow] .button-primary:focus,[foreground=google-yellow] .button-primary:hover{background:#ea8600}[background=google-yellow]{background-color:#fcc934}[foreground=google-yellow] :focus>:not(.material-icons),[foreground=google-yellow] :link>:not(.material-icons):hover,[foreground=google-yellow] a:not(.button) h2,[foreground=google-yellow] a:not(.button) h3{color:#f9ab00}.devsite-landing-row[background=google-yellow]+.devsite-landing-row[background=google-yellow]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-icon-container[foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[background][foreground=google-yellow],.devsite-landing-row-item-list-item-icon-container[foreground=google-yellow],.devsite-landing-row-item[foreground=google-yellow] :link h2,.devsite-landing-row-item[foreground=google-yellow] :link h3{color:#f9ab00}.devsite-landing-row-item-icon-container[background=google-yellow],.devsite-landing-row-item-list-item-icon-container[background=google-yellow]{background:#fcc934}[foreground=light-green] .button{color:#8bc34a}[foreground=light-green] .button:active,[foreground=light-green] .button:focus,[foreground=light-green] .button:hover{background:#f3f9ed}[foreground=light-green] .button-primary{background:#8bc34a;color:#202124}[foreground=light-green] .button-primary:active,[foreground=light-green] .button-primary:focus,[foreground=light-green] .button-primary:hover{background:#689f38}[background=light-green]{background-color:#aed581}[foreground=light-green] :focus>:not(.material-icons),[foreground=light-green] :link>:not(.material-icons):hover,[foreground=light-green] a:not(.button) h2,[foreground=light-green] a:not(.button) h3{color:#8bc34a}.devsite-landing-row[background=light-green]+.devsite-landing-row[background=light-green]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-icon-container[foreground=light-green],.devsite-landing-row-item-list-item-icon-container[background][foreground=light-green],.devsite-landing-row-item-list-item-icon-container[foreground=light-green],.devsite-landing-row-item[foreground=light-green] :link h2,.devsite-landing-row-item[foreground=light-green] :link h3{color:#8bc34a}.devsite-landing-row-item-icon-container[background=light-green],.devsite-landing-row-item-list-item-icon-container[background=light-green]{background:#aed581}[foreground=orange] .button{color:#e8710a}[foreground=orange] .button:active,[foreground=orange] .button:focus,[foreground=orange] .button:hover{background:#fdf1e7}[foreground=orange] .button-primary{background:#e8710a;color:#202124}[foreground=orange] .button-primary:active,[foreground=orange] .button-primary:focus,[foreground=orange] .button-primary:hover{background:#c26401}[background=orange]{background-color:#fcad70}[foreground=orange] :focus>:not(.material-icons),[foreground=orange] :link>:not(.material-icons):hover,[foreground=orange] a:not(.button) h2,[foreground=orange] a:not(.button) h3{color:#e8710a}.devsite-landing-row[background=orange]+.devsite-landing-row[background=orange]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=orange],.devsite-landing-row-item-icon-container[foreground=orange],.devsite-landing-row-item-list-item-icon-container[background][foreground=orange],.devsite-landing-row-item-list-item-icon-container[foreground=orange],.devsite-landing-row-item[foreground=orange] :link h2,.devsite-landing-row-item[foreground=orange] :link h3{color:#e8710a}.devsite-landing-row-item-icon-container[background=orange],.devsite-landing-row-item-list-item-icon-container[background=orange]{background:#fcad70}[foreground=white] .button{color:#fff}[foreground=white] .button:active,[foreground=white] .button:focus,[foreground=white] .button:hover{background:#fff}[foreground=white] .button-primary{background:#fff;color:#202124}[foreground=white] .button-primary:active,[foreground=white] .button-primary:focus,[foreground=white] .button-primary:hover{background:#202124}[background=white]{background-color:#fff}[foreground=white] :focus>:not(.material-icons),[foreground=white] :link>:not(.material-icons):hover,[foreground=white] a:not(.button) h2,[foreground=white] a:not(.button) h3{color:#039be5}.devsite-landing-row[background=white]+.devsite-landing-row[background=white]{padding-top:0}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white],.devsite-landing-row-item-list-item-icon-container[background][foreground=white],.devsite-landing-row-item-list-item-icon-container[foreground=white],.devsite-landing-row-item[foreground=white] :link h2,.devsite-landing-row-item[foreground=white] :link h3{color:#039be5}.devsite-landing-row-item-icon-container[background=white],.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}[background=cyan] .devsite-landing-row-description,[background=cyan] .devsite-landing-row-item-icon-container:not([foreground]),[background=cyan] :link:not(.button),[background=cyan]:not(.devsite-landing-row-cards) h3,[background=cyan]:not([foreground]):not(.devsite-landing-row-cards),[background=cyan] :visited:not(.button),[background=cyan] h2{color:#202124}[background=cyan] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=cyan] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=cyan] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=cyan] :focus .devsite-landing-row-item-icon-container[background],[background=cyan] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=cyan] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=cyan] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=cyan] :link .devsite-landing-row-item-list-item-description h4+p,[background=google-yellow] .devsite-landing-row-description,[background=google-yellow] .devsite-landing-row-item-icon-container:not([foreground]),[background=google-yellow] :link:not(.button),[background=google-yellow]:not(.devsite-landing-row-cards) h3,[background=google-yellow]:not([foreground]):not(.devsite-landing-row-cards),[background=google-yellow] :visited:not(.button),[background=google-yellow] h2{color:#202124}[background=google-yellow] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=google-yellow] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=google-yellow] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=google-yellow] :focus .devsite-landing-row-item-icon-container[background],[background=google-yellow] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=google-yellow] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=google-yellow] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=google-yellow] :link .devsite-landing-row-item-list-item-description h4+p,[background=light-green] .devsite-landing-row-description,[background=light-green] .devsite-landing-row-item-icon-container:not([foreground]),[background=light-green] :link:not(.button),[background=light-green]:not(.devsite-landing-row-cards) h3,[background=light-green]:not([foreground]):not(.devsite-landing-row-cards),[background=light-green] :visited:not(.button),[background=light-green] h2{color:#202124}[background=light-green] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=light-green] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=light-green] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=light-green] :focus .devsite-landing-row-item-icon-container[background],[background=light-green] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=light-green] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=light-green] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=light-green] :link .devsite-landing-row-item-list-item-description h4+p,[background=orange] .devsite-landing-row-description,[background=orange] .devsite-landing-row-item-icon-container:not([foreground]),[background=orange] :link:not(.button),[background=orange]:not(.devsite-landing-row-cards) h3,[background=orange]:not([foreground]):not(.devsite-landing-row-cards),[background=orange] :visited:not(.button),[background=orange] h2{color:#202124}[background=orange] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=orange] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background=orange] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=orange] :focus .devsite-landing-row-item-icon-container[background],[background=orange] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=orange] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=orange] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background=orange] :link .devsite-landing-row-item-list-item-description h4+p,[background=white] .devsite-landing-row-description,[background=white] .devsite-landing-row-item-icon-container:not([foreground]),[background=white] :link:not(.button),[background=white]:not(.devsite-landing-row-cards) h3,[background=white]:not([foreground]):not(.devsite-landing-row-cards),[background=white] :visited:not(.button),[background=white] h2{color:#202124}[background=white] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background=white] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}[background=white] .devsite-landing-row-item-list-item-icon-container:not([foreground]),[background=white] :focus .devsite-landing-row-item-icon-container[background],[background=white] :link .devsite-landing-row-item-icon-container[background]:hover{color:#202124}[background=white] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background=white] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}[background=white] :link .devsite-landing-row-item-list-item-description h4+p{color:#202124}body[theme=white] devsite-user div.devsite-user-dialog-signin .devsite-user-dialog-letter,body[theme=white] devsite-user div.devsite-user-dialog .devsite-user-dialog-photo{background-color:#1a73e8;color:#fff}body[theme=cloud-theme] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color,body[theme=white] .devsite-feedback-item-icon-container.devsite-feedback-item-icon-color{background-color:#1a73e8}devsite-content{display:block;position:relative}body[layout=docs] devsite-content{align-self:start;-ms-grid-column:3;grid-column:2;-ms-grid-row:1;grid-row:1;margin:24px 0;max-width:936px;min-width:0}body[layout=docs] .devsite-article{background-color:#fff;padding:40px}body[layout=docs][type=landing] .devsite-article{padding:0}.devsite-article-meta{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin:0 0 20px}.devsite-banner+.devsite-article-meta{margin-top:-16px}body[layout=full] .devsite-article-meta,body[type=landing] .devsite-article-meta{margin:0}devsite-feedback[position=header]{display:block;position:relative;top:-4px}[dir=ltr] devsite-feedback[position=header]{float:right;margin-left:24px}[dir=rtl] devsite-feedback[position=header]{float:left;margin-right:24px}body[layout=full] devsite-feedback[position=header],body[type=landing] devsite-feedback[position=header]{display:none}@media screen and (max-width:840px){body[layout=docs] devsite-content{margin:0}body[layout=docs] .devsite-article{padding:24px}.devsite-banner+.devsite-article-meta{margin-top:0}}@media screen and (max-width:600px){body[layout=docs] .devsite-article{padding:16px}.devsite-article-meta{display:block;margin:0 0 12px}[dir] devsite-feedback[position=header]{float:none;margin:0 0 12px;position:static}}#devsite-support-form{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:0 0 0 -40px}#devsite-support-form>*{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:0 0 0 40px}.devsite-support-form-error{color:#dd2c00}.devsite-support-form-hidden{display:none}.devsite-support-form-field{margin:0 0 8px}.devsite-support-form-field input:not([type=checkbox]):not([type=radio]),.devsite-support-form-field select,.devsite-support-form-field textarea{width:100%}.devsite-support-form-cc{color:#5f6368;display:block;font-size:13px}#devsite-support-form>.devsite-support-form-half{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}.devsite-support-quota{font:italic 400 12px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px}.devsite-support-quota-help{font-size:18px;margin-left:4px;vertical-align:top}.devsite-support-quota-help:after{content:"help";color:#bdc1c6}@media screen and (max-width:600px){#devsite-support-form{display:block}}.devsite-404-wrapper,.devsite-offline-wrapper{margin:0 auto;max-width:804px;position:relative;text-align:center}.devsite-404-header,.devsite-offline-header{margin:120px 24px 20px;position:relative;z-index:2}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin:0 0 160px;position:relative;z-index:1}.devsite-404-search devsite-search .devsite-popout-result{max-height:304px}.devsite-offline-reload{text-align:center}.devsite-404-header h3{font:400 64px/64px Roboto Mono,monospace}.devsite-offline-header h3{font:400 32px/48px Roboto Mono,monospace}.devsite-404-wrapper devsite-search,.devsite-404-wrapper devsite-search .devsite-searchbox,[dir=rtl] .devsite-404-wrapper devsite-search,[dir=rtl] .devsite-404-wrapper devsite-search .devsite-searchbox{margin:0;width:100%}.devsite-404-wrapper devsite-search .devsite-search-button{display:none}.devsite-offline-wrapper .devsite-offline-suggestions{text-align:left}[dir=rtl] .devsite-offline-wrapper .devsite-offline-suggestions{text-align:right}.devsite-offline-wrapper .devsite-offline-suggestions h3,.devsite-offline-wrapper .devsite-offline-suggestions ul{margin:0}.devsite-404-wrapper .devsite-404-links{border-top:1px solid #dadce0;margin:0 calc(50% - 50vw) 40px;padding:0 calc(50vw - 50%);text-align:left}[dir=rtl] .devsite-404-wrapper .devsite-404-links{text-align:right}.devsite-404-wrapper .devsite-404-links ul{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;list-style:none;padding:0}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4);margin-left:24px}[dir=rtl] .devsite-404-wrapper .devsite-404-links li{margin-left:0;margin-right:24px}.devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(4n+1){margin-right:0}@media screen and (max-width:840px){.devsite-404-header,.devsite-offline-header{margin-top:40px}.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:80px}.devsite-404-wrapper .devsite-404-links li{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-left:0}[dir=rtl] .devsite-404-wrapper .devsite-404-links li:nth-of-type(odd){margin-right:0}}@media screen and (max-width:600px){.devsite-404-search,.devsite-offline-reload,.devsite-offline-suggestions{margin-bottom:40px}}devsite-dynamic-content .devsite-card-image-bg{background-image:url(../images/dynamic-content-card-default.png)}@media print{.caution,.caution a,.devsite-banner,.devsite-banner a,.dogfood,.dogfood a,.key-point,.key-point a,.key-term,.key-term a,.note,.note a,.objective,.objective a,.prettyprint a,.special,.special a,.success,.success a,.warning,.warning a,:link,:visited,a .atn,a .atv,a .com,a .dec,a .kwd,a .lit,a .pln,a .pun,a .str,a .tag,a .typ,a code,aside,aside :link,aside :visited,body,code,h1,h1 code,h2,h2 code,h3,h3 code,h4,h4 code,h5,h5 code,h6,h6 code,html,pre,pre .atn,pre .atv,pre .com,pre .dec,pre .kwd,pre .lit,pre .pln,pre .pun,pre .str,pre .tag,pre .typ,td,td code,th,th :link,th :visited,th code,var{color:#000!important;padding-left:0!important;padding-right:0!important}#gc-wrapper{margin:0!important}devsite-expandable>:not(.showalways):not(.exw-control):not(.exw-expanded-content):not(.expand-control){display:block!important}:link,:visited{text-decoration:underline}.devsite-article-meta,.devsite-banner-confidential .button,.devsite-book-nav-bg,.devsite-code-buttons-container,devsite-book-nav,devsite-feedback,devsite-footer-linkboxes,devsite-footer-promos,devsite-footer-utility,devsite-googler-buttons,devsite-header,devsite-page-rating,devsite-toc{display:none!important}.devsite-article,.devsite-main-content,devsite-content{background:0!important;border:0!important;-webkit-box-shadow:none!important;box-shadow:none!important;display:block!important;margin:0!important;max-width:none!important;padding:0!important;width:auto!important}.devsite-banner{margin-top:0}.attempt-left,.attempt-right,.video-wrapper{float:none;margin:16px 0}img,video{display:block!important;page-break-inside:avoid!important}.devsite-main-content a[href]:after{content:" (" attr(href) ")";display:inline-block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:100%;word-wrap:break-word}}@page{margin:.75in}.devsite-product-platform-row{margin:8px 0;padding:0 24px}.devsite-header-no-lower-tabs .devsite-product-platform-row{margin-top:-12px;padding-bottom:24px}.devsite-platform-container{display:inline-block}.devsite-platform-container+.devsite-platform-container{margin-left:16px}.devsite-platform-icon-container{background:#fff;-webkit-border-radius:50%;border-radius:50%;height:40px;margin:0 auto;width:40px}.devsite-platform-icon{color:#5f6368;font-size:24px;height:24px;margin:8px;width:24px}@media screen and (max-width:1000px){div.devsite-collapsible-section,div.devsite-header-background{background-image:none}}@media screen and (max-width:600px){.devsite-product-platform-row{padding:0 16px}}.devsite-landing-row{padding:40px 0}.devsite-landing-row-group,.devsite-landing-row-html{margin:0 auto;max-width:1520px;padding:0 40px}.devsite-landing-row:not([background]){background-color:#fff}.devsite-landing-row:not([background])+.devsite-landing-row:not([background]),.devsite-landing-row[background=grey]+.devsite-landing-row[background=grey],.devsite-landing-row[background=theme]+.devsite-landing-row[background=theme]{padding-top:0}.devsite-landing-row:not([background])+.devsite-landing-row.devsite-landing-row-cta{padding-top:40px}@media screen and (max-width:840px){.devsite-landing-row{padding:24px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row{padding:16px 0}.devsite-landing-row-group,.devsite-landing-row-html{padding:0 16px}}.devsite-landing-row-cta{text-align:center}.devsite-landing-row-cta .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-cta h3{font:300 34px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;letter-spacing:-.01em;margin-bottom:16px}.devsite-landing-row-cta h3+.devsite-landing-row-item-buttons{margin-top:8px}.devsite-landing-row-cta.devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0}.devsite-landing-row-header{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end;margin:0 auto 32px;max-width:1520px;padding:0 40px}.devsite-landing-row-header+.devsite-landing-row-group{margin-top:32px}.devsite-landing-row-header-text{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-header-text>h2{margin:0}.devsite-landing-row-description{font:18px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#5f6368}h2+.devsite-landing-row-description{margin-top:16px}.devsite-landing-row-header-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;margin:-4px}.devsite-landing-row-header-buttons>.button{margin:4px}.devsite-landing-row-header-centered .devsite-landing-row-header{display:block;text-align:center}.devsite-landing-row-header-centered .devsite-landing-row-description{margin-left:auto;margin-right:auto;max-width:856px}.devsite-landing-row-header-centered .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:24px}@media screen and (max-width:840px){.devsite-landing-row-header{padding:0 24px}}@media screen and (max-width:600px){.devsite-landing-row-header{display:block;padding:0 16px}.devsite-landing-row-header-text+.devsite-landing-row-header-buttons{display:block;margin:16px 0 0 -4px}}.devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:32px}.devsite-landing-row-column,.devsite-landing-row-item{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-width:100%;min-width:0}.devsite-landing-row-item-hidden{visibility:hidden}.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-column:not(:first-child),.devsite-landing-row-item:not(:first-child){margin-left:24px}[dir=rtl] .devsite-landing-row-column:not(:first-child),[dir=rtl] .devsite-landing-row-item:not(:first-child){margin-left:0;margin-right:24px}.devsite-landing-row-column>.devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0}[dir=rtl] .devsite-landing-row-column>.devsite-landing-row-item{margin-right:0}@media screen and (max-width:840px){.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(3){margin:24px 0 0}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(4),[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin:24px 24px 0 0}.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:0 24px 0 0}.devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{width:100%}.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;margin-left:0}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description{display:block}.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item.devsite-landing-row-item-no-description .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item{-webkit-flex-basis:auto;flex-basis:auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:-webkit-calc((100% - 24px)/2);width:calc((100% - 24px)/2)}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2){margin:0 24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-cards .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden:nth-of-type(n+3){display:none}}@media screen and (max-width:600px){.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-group{display:block}.devsite-landing-row-1-up .devsite-landing-row-column,.devsite-landing-row-1-up .devsite-landing-row-item,.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media){-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-2-up .devsite-landing-row-column,.devsite-landing-row-3-up .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column,.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-column:nth-of-type(2n),.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2n),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child),.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:24px 0 0}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-flex-basis:100%;flex-basis:100%}.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2)}.devsite-landing-row-logos .devsite-landing-row-column:nth-child(n+3),.devsite-landing-row-logos .devsite-landing-row-item:nth-child(n+3){margin:24px 0 0}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column,.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 32px)/3);flex-basis:calc((100% - 32px)/3)}.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-column:not(:first-child),.devsite-landing-row-3-up.devsite-landing-row-logos .devsite-landing-row-item:not(:first-child){margin:0}.devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:24px}[dir=rtl] .devsite-landing-row-logos .devsite-landing-row-item:nth-child(2n){margin-left:0;margin-right:24px}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{width:100%}.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3),.devsite-landing-row-4-up.devsite-landing-row-logos .devsite-landing-row-item-hidden:nth-of-type(3)~.devsite-landing-row-item-hidden,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-hidden{display:none}}.devsite-landing-row-item-media{margin:0 0 32px;min-width:0}.devsite-landing-row-item-code devsite-code,.devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-item-video{display:block}.devsite-landing-row-item[background] .devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-body,.devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;min-width:0;width:100%}.devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto 0 -8px -12px;padding-top:8px}[dir=rtl] .devsite-landing-row-item .devsite-landing-row-item-buttons{margin:auto -12px -8px 0}.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((200% - 40px)/3);flex:0 0 calc((200% - 40px)/3);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row-1-up .devsite-landing-row-item-description{display:block;-webkit-box-flex:0;-webkit-flex:0 1 -webkit-calc((200% - 40px)/3);flex:0 1 calc((200% - 40px)/3);margin:0 24px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-75 .devsite-landing-row-item-description,.devsite-landing-row-100 .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.devsite-landing-row-1-up.devsite-landing-row-100 .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-media-left{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 40px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 24px 0 0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description,.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 40px}.devsite-landing-row-50 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-67 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-75 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}.devsite-landing-row-100 .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-item-description h2,.devsite-landing-row-large-headings h3,.devsite-landing-row h3:first-child,.devsite-landing-row h4+p,.devsite-landing-row h4:first-child,.devsite-landing-row h5:first-child,.devsite-landing-row h6:first-child,.devsite-landing-row p:first-child{margin-top:0}.devsite-landing-row-item-description-content>:last-child{margin-bottom:0}.devsite-landing-row-item-centered .devsite-landing-row-item-description-content,.devsite-landing-row-item-centered h3{text-align:center}.devsite-landing-row-item-centered .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2);margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;overflow:hidden}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:unset;-webkit-flex:unset;flex:unset;margin:0 0 32px}.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item[background] .devsite-landing-row-item-media{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row .devsite-landing-row-item-description{display:block;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-description,[dir=rtl] .devsite-landing-row .devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-4-up .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}.devsite-landing-row-3-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 48px)/3);flex:0 0 calc((100% - 48px)/3)}.devsite-landing-row-1-up .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-item-no-media .devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{-webkit-box-ordinal-group:2;-webkit-order:1;order:1;margin:0 24px 0 0}[dir=rtl] .devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0 0 0 24px}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{margin:0;padding:24px}.devsite-landing-row-logos .devsite-landing-row-item-description{margin:0}.devsite-landing-row-100 .devsite-landing-row-item-media,.devsite-landing-row-100:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:32px 0 0}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description,.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-column .devsite-landing-row-item-description{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}}@media screen and (max-width:600px){.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-1-up .devsite-landing-row-item-media-left,.devsite-landing-row-item-media,.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:0 0 32px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-1-up .devsite-landing-row-item-description,.devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-1-up .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}.devsite-landing-row-1-up .devsite-landing-row-item[background] .devsite-landing-row-item-media+.devsite-landing-row-item-description{padding:16px}.devsite-landing-row-item-no-media:not(:first-child),.devsite-landing-row .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n){margin:24px 0 0}.devsite-landing-row-cta .devsite-landing-row-item-description{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.devsite-landing-row-item-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin-left:-12px;padding-top:8px}.devsite-landing-row-item-buttons .button{margin:4px 4px 4px 12px}.devsite-landing-row-item-buttons .button>.material-icons{top:-1px}.devsite-landing-row-item-buttons .button-white:not(.button-raised),.devsite-landing-row-item-buttons .button-white:not(.button-raised)+.button-white:not(.button-raised){margin:0 4px}.devsite-landing-row-cta .devsite-landing-row-item-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0 0}p+.devsite-landing-row-item-buttons{margin-top:-8px}.devsite-landing-row-item-custom-image{padding:0 0 56.25%;position:relative}.devsite-landing-row-item-image.devsite-landing-row-item-custom-image:not([background]){background:#455a64}.devsite-landing-row-item-custom-image[background=grey]{background:#f1f3f4}.devsite-landing-row-item-custom-image[background=white]{background:#fff}.devsite-landing-row-no-image-background .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-item-custom-image-icon-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:absolute;width:100%}.devsite-landing-row-item-custom-image-icon-container[background]{background:0}.devsite-landing-row-item-custom-image-icon{color:#fff}.devsite-landing-row-item-custom-image[background=grey]:not([foreground]) .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-item-custom-image[background=white]:not([foreground]) .devsite-landing-row-item-custom-image-icon{color:#5f6368}.devsite-landing-row-item-custom-image-icon.material-icons{opacity:.8}.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon{font-size:256px;max-height:256px;width:256px}.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-50 .devsite-landing-row-item-custom-image-icon{font-size:192px;max-height:192px;width:192px}.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-67 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-75 .devsite-landing-row-item-custom-image-icon,[layout=docs] .devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon{font-size:96px;max-height:96px;width:96px}@media screen and (max-width:840px){.devsite-landing-row-1-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-2-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-3-up .devsite-landing-row-item-custom-image-icon,.devsite-landing-row-4-up .devsite-landing-row-item-custom-image-icon{font-size:128px;max-height:128px;width:128px}}.devsite-landing-row-1-up .devsite-landing-row-item-description[icon-position=left],.devsite-landing-row-item-description[icon-position]{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-row-item-description[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:56px;margin:0 20px 8px 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:56px}[dir=rtl] .devsite-landing-row-item-icon-container{margin:0 0 8px 20px}.devsite-landing-row-item-icon{font-size:48px;height:48px;margin:0 0 0 4px;width:48px}[dir=rtl] .devsite-landing-row-item-icon{margin:0 4px 0 0}.devsite-landing-row-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-bottom:20px}.devsite-landing-row-item-icon-container[background] .devsite-landing-row-item-icon{font-size:36px;height:36px;margin:0;width:36px}[background] .devsite-landing-row-item-icon-container:not([foreground]){color:#fff}[background=grey] .devsite-landing-row-item-icon-container:not([foreground]){color:#5f6368}:focus .devsite-landing-row-item-icon-container,:link .devsite-landing-row-item-icon-container:hover{color:#1a73e8}[foreground] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[foreground] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:rgba(154,160,166,.5)}[background] .devsite-landing-row-item :focus .devsite-landing-row-item-icon-container,[background] .devsite-landing-row-item :link .devsite-landing-row-item-icon-container:hover{color:hsla(0,0%,100%,.7)}.devsite-landing-row-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-icon-container[background][foreground=white],.devsite-landing-row-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-icon-container[background][foreground=white]:hover{color:#fff}:focus .devsite-landing-row-item-icon-container[background],:link .devsite-landing-row-item-icon-container[background]:hover{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}[background] :focus .devsite-landing-row-item-icon-container[background],[background] :link .devsite-landing-row-item-icon-container[background]:hover{color:#fff}[background=grey] :focus .devsite-landing-row-item-icon-container[background],[background=grey] :link .devsite-landing-row-item-icon-container[background]:hover{color:#5f6368}.devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start}.devsite-landing-row-item-image:not([background]){background:#e8eaed}.devsite-landing-row-item-image img{vertical-align:middle;width:100%}.devsite-landing-row-item-image a{display:block}.devsite-landing-row-no-image-background .devsite-landing-row-item-image:not([background]){background:0}.devsite-landing-row-item-labels>a,.devsite-landing-row-item-labels>span{display:inline-block;font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin-bottom:8px;text-transform:uppercase}.devsite-landing-row-item-labels>a+a,.devsite-landing-row-item-labels>a+span,.devsite-landing-row-item-labels>span+a,.devsite-landing-row-item-labels>span+span{margin-left:8px}.devsite-landing-row-item-labels>a[background],.devsite-landing-row-item-labels>span[background]{-webkit-border-radius:4px;border-radius:4px;margin-bottom:16px;padding:4px 8px}.devsite-landing-row-item-description-callout{font-weight:700}.devsite-landing-row-item-description-feature{margin-top:16px;position:relative}.devsite-landing-row-item-description-feature+.devsite-landing-row-item-description-feature{margin:0}.devsite-landing-row-item-description-feature-link{border-bottom:1px solid #e8eaed;font-weight:500;padding:12px 0 11px}.devsite-landing-row-item-description-feature-tooltip{background:#455a64;color:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;min-width:200px;opacity:0;padding:24px;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;visibility:hidden;width:67%;z-index:1020}.no-touch .devsite-landing-row-item-description-feature-link:hover+.devsite-landing-row-item-description-feature-tooltip{opacity:1;visibility:visible}.devsite-landing-row-item-description-feature-tooltip:before{border-bottom:8px solid #455a64;border-left:8px solid transparent;border-right:8px solid transparent;content:"";position:absolute;top:-8px}.devsite-landing-row-item-description-feature-tooltip h3,.devsite-landing-row-large-headings .devsite-landing-row-item-description-feature-tooltip h3{color:#fff;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:8px;padding:0}.devsite-landing-row-item-list{padding:0}.devsite-landing-row-item-description-content+.devsite-landing-row-item-list{margin-top:32px}.devsite-landing-row-item-list-item{list-style:none}.devsite-landing-row-item-list-item-content{display:-webkit-box;display:-webkit-flex;display:flex}.devsite-landing-row-item-list-item-content[icon-position=top]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-item-list-item-icon-container{color:#5f6368;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:40px;margin:0 20px 0 0;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s;width:40px}[dir=rtl] .devsite-landing-row-item-list-item-icon-container{margin:0 0 0 20px}.devsite-landing-row-item-list-item-icon{font-size:32px;height:32px;width:32px}.devsite-landing-row-item-list-item-icon-container[background]{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.devsite-landing-row-item-list-item-icon-container[background] .devsite-landing-row-item-list-item-icon{font-size:24px;height:24px;width:24px}.devsite-landing-row-item-list h4{font:400 16px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-landing-row .devsite-landing-row-item-list h4{margin:0 0 4px}.devsite-landing-row-item-list-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-width:-webkit-calc(100% - 40px);max-width:calc(100% - 40px)}@media screen and (max-width:840px){.devsite-landing-row-item-list-item-description{max-width:none}}.devsite-landing-row-item-list-item:not(:last-child) .devsite-landing-row-item-list-item-description-content{margin-bottom:24px}[background] .devsite-landing-row-item-list-item-icon-container:not([foreground]){color:#fff}.devsite-landing-row-item-list-item-icon-container[background=grey]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background=white]:not([foreground]),.devsite-landing-row-item-list-item-icon-container[background][foreground=grey],.devsite-landing-row-item-list-item-icon-container[foreground=grey]{color:#5f6368}.devsite-landing-row-item-list-item-icon-container[background=grey]{background:#f1f3f4}.devsite-landing-row-item-list-item-icon-container[background=white]{background:#fff}.devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white]{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[foreground=grey]:hover{color:rgba(154,160,166,.5)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[foreground=white],:link .devsite-landing-row-item-list-item-icon-container[foreground=white]:hover{color:hsla(0,0%,100%,.7)}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=grey],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=grey]:hover{color:#5f6368}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background][foreground=white],:link .devsite-landing-row-item-list-item-icon-container[background][foreground=white]:hover{color:#fff}.devsite-landing-row-item-list-item a:focus,:link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover,[background] :link>.devsite-landing-row-item-list-item-content:not(.material-icons):hover{text-decoration:none}[foreground] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[foreground] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:rgba(154,160,166,.5)}[background] :focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container,[background] :link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container{color:hsla(0,0%,100%,.7)}:link .devsite-landing-row-item-list-item-description h4+p{color:#202124;text-decoration:none}[background] :link .devsite-landing-row-item-list-item-description h4+p{color:#fff}:focus .devsite-landing-row-item-list-item-content .devsite-landing-row-item-list-item-icon-container[background],:link .devsite-landing-row-item-list-item-content:hover .devsite-landing-row-item-list-item-icon-container[background]{background:hsla(0,0%,100%,.7);-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.devsite-landing-row :focus .devsite-landing-row-item-list-item-description>:first-child,[background] :link .devsite-landing-row-item-list-item-description>:first-child{text-decoration:underline}.devsite-landing-row-logos .devsite-landing-row-item-media{margin:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image{padding-bottom:96px}body[theme] .devsite-landing-row-logos .devsite-landing-row-item-custom-image:not([background]){background:0}.devsite-landing-row-logos .devsite-landing-row-item-custom-image-icon{opacity:1;width:96px}.devsite-landing-row-logos .devsite-landing-row-item-description{text-align:center}.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 72px)/4);flex:0 0 calc((100% - 72px)/4)}@media screen and (max-width:600px){.devsite-landing-row-logos .devsite-landing-row-item-no-media{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc((100% - 24px)/2);flex:0 0 calc((100% - 24px)/2)}}.devsite-landing-row-cards .devsite-landing-row-item{-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);overflow:hidden;position:relative}.devsite-landing-row-cards .devsite-landing-row-item:not([background]){background-color:#fff}body[theme] .devsite-landing-row-cards[background=theme] :link:not(.button),body[theme] .devsite-landing-row-cards[background=theme] :visited:not(.button){color:#1a73e8}.devsite-landing-row-cards .devsite-landing-row-column{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:not(:first-child){margin-top:24px}.devsite-landing-row-cards .devsite-landing-row-column .devsite-landing-row-item{display:block;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}.devsite-landing-row-cards .devsite-landing-row-item-media{margin-bottom:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.devsite-landing-row-cards .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:16px}.devsite-landing-row-cards .devsite-landing-row-item-buttons{padding-top:16px}.devsite-landing-row-cards .devsite-landing-row-item-no-media{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards[background=theme] .devsite-landing-row-item-icon{color:#5f6368}.devsite-landing-row-cards .devsite-landing-row-item-no-media h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 20px}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;margin:0}@media screen and (max-width:840px){.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item-media,.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;margin:0}.devsite-landing-row-cards.devsite-landing-row-1-up .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(.devsite-landing-row-item-no-media),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 24px)/2);flex-basis:calc((100% - 24px)/2);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 0 0 24px}[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2),[dir=rtl] .devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-child(2){margin:0 24px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2){margin:24px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}}@media screen and (max-width:600px){.devsite-landing-row-cards{-webkit-flex-basis:100%;flex-basis:100%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding-bottom:16px;padding-top:16px}.devsite-landing-row-cards+.devsite-landing-row-cards .devsite-landing-row-header{padding-top:16px}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column>.devsite-landing-row-item:nth-child(2),.devsite-landing-row-cards .devsite-landing-row-column>.devsite-landing-row-item:nth-child(n+2),.devsite-landing-row-cards:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item,.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%}.devsite-landing-row-cards.devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-3-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child),.devsite-landing-row-cards.devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:16px 0 0}.devsite-landing-row-cards .devsite-landing-row-item-no-media .devsite-landing-row-item-description{padding-top:16px}}.devsite-landing-row-1-up.devsite-landing-row-marquee,.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-header{margin:0;padding:40px 0 0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;margin:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-image{background:0}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{-webkit-align-self:center;align-self:center;padding:40px 0}@media screen and (max-width:840px){.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media{margin:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;overflow:visible;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-description{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;width:100%}.devsite-landing-row-1-up.devsite-landing-row-marquee .devsite-landing-row-item-media-left+.devsite-landing-row-item-description{margin:0}}@media screen and (max-width:600px){.devsite-landing-row-1-up.devsite-landing-row-marquee[background]{margin:0 -16px;padding:0}.devsite-landing-row-1-up.devsite-landing-row-marquee[background] .devsite-landing-row-item-description{padding:32px 16px}}body[layout=full] devsite-code{overflow:visible}body[layout=full] devsite-code:after{background:#f1f3f4;content:"";display:block;height:100%;left:-webkit-calc(50% - 50vw);left:calc(50% - 50vw);position:absolute;top:0;width:100vw;z-index:-1}body[layout=full] devsite-code[dark-code]:after{background:#283142}[background] :link:not(.button),[background] :visited:not(.button){color:inherit}body[type=landing][layout=docs] h2{border-bottom:0;padding-bottom:0}[background]:not([background=grey]) :link>:not(.material-icons):hover,[background]:not([background=grey]) p>a:not(.button){text-decoration:none}.dac-center{-webkit-box-flex:1;-webkit-flex:1;flex:1;text-align:center}.dac-grow-1{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-grow-2{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}.dac-grow-3{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}.dac-grow-4{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-grow-5{-webkit-box-flex:5;-webkit-flex-grow:5;flex-grow:5}.dac-grow-6{-webkit-box-flex:6;-webkit-flex-grow:6;flex-grow:6}.dac-grow-7{-webkit-box-flex:7;-webkit-flex-grow:7;flex-grow:7}.dac-grow-8{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-grow-9{-webkit-box-flex:9;-webkit-flex-grow:9;flex-grow:9}.dac-grow-10{-webkit-box-flex:10;-webkit-flex-grow:10;flex-grow:10}.dac-grow-11{-webkit-box-flex:11;-webkit-flex-grow:11;flex-grow:11}.dac-grow-12{-webkit-box-flex:12;-webkit-flex-grow:12;flex-grow:12}.dac-full-width{width:100%}.dac-hidden-sm-up,.dac-hidden-xs-down.dac-hidden-sm-up,.dac-hidden-xs-up{display:none!important}.dac-flex-spread{-webkit-box-align:baseline;-webkit-align-items:baseline;align-items:baseline;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}@media screen and (max-width:720px){.dac-hidden-xs-down{display:none!important}.dac-hidden-sm-up,.dac-hidden-xs-up{display:block!important}}@media screen and (max-width:1000px){.dac-hidden-sm-down{display:none!important}}@media screen and (min-width:720px) and (max-width:1000px){.dac-hidden-xs-down.dac-hidden-sm-up{display:block!important}}.dac-landing-row-item-height-small{min-height:350px}.dac-landing-row-item-height-medium{min-height:500px}.dac-landing-row-item-height-large{min-height:750px}@media screen and (max-width:1000px){.dac-landing-row-item-height-small{min-height:inherit}.dac-landing-row-item-height-large,.dac-landing-row-item-height-medium{min-height:250px}}.dac-heading-hidden h1,.dac-heading-hidden h2,.dac-heading-hidden h3,.dac-heading-hidden h4,.dac-heading-hidden h5{display:none}.dac-banner-card,.dac-banner-card.dac-landing-row-bg-slate,.dac-landing-row-bg-blob-8,.dac-landing-row.dac-landing-row-collapse{background-clip:content-box}.devsite-landing-row:not([background]).dac-landing-row-bg-blob-8,[background=grey]{background-color:#f7f9fa}.dac-landing-row-bg-mint{background-color:#55ffb5}.dac-landing-row-bg-yellow{background-color:#ffd600}.dac-landing-row-bg-mint,.dac-landing-row-bg-yellow .button.dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint:focus,.dac-landing-row-bg-mint:hover,.dac-landing-row-bg-yellow .button.dac-outline-button:focus,.dac-landing-row-bg-yellow .button.dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.dac-landing-row-bg-blue{background-color:#2196f3}.dac-landing-row-bg-blue:link:not(.dac-button),[background=grey]:not(.devsite-landing-row-cards) .dac-landing-row-bg-slate h3{color:#fff}.dac-landing-row-bg-slate,.devsite-landing-row:not([background]).dac-landing-row-bg-slate{background-color:#455a64}.dac-landing-row-bg-green,.dac-landing-row-item-description-bg-green .devsite-landing-row-item-description{background-color:#4b7d2f}.dac-landing-row-bg-blue .dac-button,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-blue .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-blue .devsite-landing-row-item-description-content p :visited,.dac-landing-row-bg-green .dac-button,.dac-landing-row-bg-green .devsite-landing-row-item-body h2,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-body h3,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-green .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-green .devsite-landing-row-item-description-content,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-green .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .dac-button,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h2 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h3 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :link,.dac-landing-row-bg-slate .devsite-landing-row-item-body h4 :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content :visited,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :link,.dac-landing-row-bg-slate .devsite-landing-row-item-description-content p :visited{color:#fff}.dac-landing-row-bg-blue .dac-button:focus,.dac-landing-row-bg-blue .dac-button:hover,.dac-landing-row-bg-green .dac-button:focus,.dac-landing-row-bg-green .dac-button:hover,.dac-landing-row-bg-slate .dac-button:focus,.dac-landing-row-bg-slate .dac-button:hover{opacity:.4}.dac-landing-row-bg-blue .dac-outline-button,.dac-landing-row-bg-green .dac-outline-button,.dac-landing-row-bg-slate .dac-outline-button{border-color:#fff}.dac-landing-row-bg-blue .dac-outline-button:focus,.dac-landing-row-bg-blue .dac-outline-button:hover,.dac-landing-row-bg-green .dac-outline-button:focus,.dac-landing-row-bg-green .dac-outline-button:hover,.dac-landing-row-bg-slate .dac-outline-button:focus,.dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;border-color:#fff;color:#414141;opacity:1}.dac-landing-row-bg-mint .dac-outline-button,.dac-landing-row-bg-yellow .dac-outline-button{border-color:#414141;color:#414141}.dac-landing-row-bg-mint .dac-outline-button:focus,.dac-landing-row-bg-mint .dac-outline-button:hover,.dac-landing-row-bg-yellow .dac-outline-button:focus,.dac-landing-row-bg-yellow .dac-outline-button:hover{background:#414141;border-color:#414141;color:#fff}.devsite-banner-announcement,.devsite-banner-announcement :link,.devsite-banner-announcement :visited{background:#eff7cf}.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row .devsite-landing-row-item-description{padding:24px}}.dac-banner-card.devsite-landing-row .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}@media screen and (max-width:720px){.dac-banner-card.devsite-landing-row img{display:block}}.dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item{background-repeat:no-repeat;-o-background-size:cover;background-size:cover;margin-left:0}.dac-banner{text-align:center}.dac-banner .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center}.dac-banner .devsite-landing-row-item-description{max-width:750px}.dac-banner .devsite-landing-row-item-body h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-banner .devsite-landing-row-item-body h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-banner .devsite-landing-row-item-description-content{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-banner-wide{background-color:transparent;background-repeat:no-repeat}.dac-banner-wide img{display:none}@media screen and (max-width:720px){.dac-banner-wide img{display:block;margin:24px auto;width:70%}}.dac-banner-wide .devsite-landing-row-item-no-media .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-banner-wide .devsite-landing-row-item{max-width:50%;padding:32px 0}@media screen and (max-width:720px){.dac-banner-wide .devsite-landing-row-item{padding:0;max-width:none}}.dac-heading-linked.devsite-landing-row-item .devsite-landing-row-item-buttons,.dac-landing-row-item-buttons-bottom .devsite-landing-row-item-buttons{padding-top:32px}.dac-button,.dac-flat-button-muted{-webkit-box-shadow:none;box-shadow:none;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:48px;letter-spacing:.5px;padding:0 24px;-webkit-transition:background-color .2s,color .2s,-webkit-box-shadow .2s;transition:background-color .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s;transition:background-color .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}.dac-button{background:#4b7d2f;color:#fff}.dac-button:focus,.dac-button:hover{background:#2f4d1f}.dac-flat-button-muted{background:#f7f9fa;color:#000}.dac-flat-button-muted:focus,.dac-flat-button-muted:hover{background:#d8e1e6}.dac-button+.dac-button{margin-left:32px}.dac-outline-button{background:0;border:2px solid #4b7d2f;-webkit-border-radius:4px;border-radius:4px;color:#4b7d2f}.dac-outline-button:focus,.dac-outline-button:hover{background:#4b7d2f;color:#fff}.dac-outline-button:disabled{background:0;border-color:#d7d7d7;color:#d7d7d7}.dac-alt-flat-button,.dac-flat-button{background:0;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0}.dac-alt-flat-button:focus,.dac-alt-flat-button:hover,.dac-flat-button:focus,.dac-flat-button:hover{background:0;opacity:.4}@media screen and (max-width:720px){.dac-alt-flat-button,.dac-flat-button{font:500 14px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-flat-button{color:#414141}.devsite-product-button-row .dac-header-button{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);background:#4b7d2f}.dac-button.dac-flat-button{height:auto;line-height:inherit;white-space:inherit}.dac-alt-flat-button{color:#4b7d2f}.dac-button-group .dac-button{margin-bottom:16px}.dac-button-group .dac-button:not(:last-child){margin-right:16px}.dac-full-width-content .devsite-landing-row-item-buttons{display:block}.dac-full-width-media .devsite-landing-row-item-buttons{display:none}.dac-resource-widget-more{padding:32px 0 0;text-align:center}@media screen and (max-width:600px){.dac-full-width-content .devsite-landing-row-item-buttons{display:none}.dac-full-width-media .devsite-landing-row-item-buttons{display:block}}.dac-jetpack:before,.dac-rss:before{content:"";height:40px;width:40px}.dac-jetpack:before{background:url(../images/custom/jetpack-icon.svg) 50%/40px no-repeat}.dac-rss:before{background:url(../images/custom/rss-icon.png) 50%/24px no-repeat}.dac-jetpack a,.dac-rss a{text-decoration:underline}.dac-jetpack a:focus,.dac-jetpack a:hover,.dac-rss a:focus,.dac-rss a:hover{text-decoration:none}.dac-jetpack a:focus,.dac-rss a:focus{background:rgba(26,115,232,.1);-webkit-border-radius:2px;border-radius:2px;margin:-4px;padding:4px}div.dac-jetpack,div.dac-rss,span.dac-jetpack{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#414141;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font-size:14px;font-weight:700;margin:0;vertical-align:text-bottom}[dir=ltr] div.dac-jetpack:before,[dir=ltr] div.dac-rss:before,[dir=ltr] span.dac-jetpack:before{margin-right:4px}[dir=rtl] div.dac-jetpack:before,[dir=rtl] div.dac-rss:before,[dir=rtl] span.dac-jetpack:before{margin-left:4px}[dir=ltr] div.dac-jetpack,[dir=ltr] div.dac-rss{padding-left:16px}[dir=rtl] div.dac-jetpack,[dir=rtl] div.dac-rss{padding-right:16px}span.dac-jetpack{position:relative}[dir=ltr] span.dac-jetpack{padding-left:12px}[dir=rtl] span.dac-jetpack{padding-right:12px}span.dac-jetpack:after{background:#414141;content:"";display:block;height:70%;margin:0;position:absolute;top:15%;width:2px}[dir=ltr] span.dac-jetpack:after{left:0}[dir=rtl] span.dac-jetpack:after{right:0}h2 span.dac-jetpack{margin-bottom:-6px}h3 span.dac-jetpack,h4 span.dac-jetpack{margin-bottom:-8px}h4 span.dac-jetpack>span{position:relative;top:3px}aside.dac-jetpack,aside.dac-jetpack :link,aside.dac-jetpack :visited,aside.dac-rss,aside.dac-rss :link,aside.dac-rss :visited{background:#e0f2f1;color:#00796b}aside.dac-jetpack :focus,aside.dac-jetpack :hover,aside.dac-rss :focus,aside.dac-rss :hover{background:hsla(0,0%,100%,.7)}aside.dac-jetpack code,aside.dac-rss code{background:0;color:#00796b}aside.dac-jetpack:before,aside.dac-rss:before{margin:-8px 0 0}[dir=ltr] aside.dac-jetpack:before,[dir=ltr] aside.dac-rss:before{margin-left:-44px}[dir=rtl] aside.dac-jetpack:before,[dir=rtl] aside.dac-rss:before{margin-right:-44px}.dac-card-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;margin-top:85px;padding:52px 32px}@media screen and (max-width:1000px){.dac-card-footer{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:32px}}.dac-card-footer img{margin:0 10px;max-width:50%}@media screen and (max-width:1000px){.dac-card-footer img{margin-bottom:32px;max-width:240px}}.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{color:#414141;font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:720px){.devsite-landing-row-item-description-content,.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-dynamic-content-no-img .devsite-card-image-bg{display:none}.dac-dynamic-content-stack devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 100%;flex:0 1 100%}.dac-dynamic-content-section .devsite-landing-row-item,.dac-resource-widget-section .devsite-landing-row-item{display:block}devsite-dynamic-content .resource-widget[class*=col-]{float:none!important;padding:0!important}devsite-dynamic-content .devsite-card-group{margin:-32px 0 0 -32px}devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(33.33% - 32px);flex:0 0 calc(33.33% - 32px);margin:32px 0 0 32px;padding:0}.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}devsite-dynamic-content .devsite-card-content{padding:32px}devsite-dynamic-content .devsite-card{background:#f7f9fa;-webkit-box-shadow:none;box-shadow:none}.devsite-card-list-item,devsite-dynamic-content .devsite-card,devsite-dynamic-content .devsite-card-list-item{-webkit-border-radius:0;border-radius:0}devsite-dynamic-content .devsite-card h3{color:#202124}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link,devsite-dynamic-content .devsite-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.devsite-card-list-link,devsite-dynamic-content .devsite-card-list-link{background:#f7f9fa;color:#80868b}.devsite-card-list-link:focus,.devsite-card-list-link:hover,devsite-dynamic-content .devsite-card-list-link:focus,devsite-dynamic-content .devsite-card-list-link:hover{background:#455a64;color:#fff}devsite-dynamic-content .devsite-card-category{font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}devsite-dynamic-content .devsite-pagination-less-button,devsite-dynamic-content .devsite-pagination-more-button{background:#4b7d2f;-webkit-box-shadow:none;box-shadow:none;color:#fff;font:500 16px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:46px;padding:0 24px}devsite-dynamic-content .devsite-pagination-less-button:focus,devsite-dynamic-content .devsite-pagination-less-button:hover,devsite-dynamic-content .devsite-pagination-more-button:focus,devsite-dynamic-content .devsite-pagination-more-button:hover{background:#2f4d1f}devsite-dynamic-content [dynamic-card-style=small] .devsite-card-content{padding:20px}devsite-dynamic-content .devsite-card[dynamic-card-type=medium]{background:rgba(0,0,0,.94)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium],devsite-dynamic-content .devsite-card[dynamic-card-type=medium] h3{color:#fff}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{border:0;padding:12px 32px 32px 80px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-repeat:no-repeat;content:"";height:40px;left:32px;position:absolute;width:40px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before{background-image:url(../images/custom/android-developers-logo.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before{background-image:url(../images/custom/android-blog-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before{background-image:url(../images/custom/medium-round-icon.svg)}devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{background-image:url(../images/custom/youtube-round-icon.svg)}@media screen and (max-width:1000px){.dac-full-width-page devsite-dynamic-content .devsite-card-wrapper,body devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}}@media screen and (min-width:720px) and (max-width:1000px){[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card>*{width:50%}[type=landing]:not(.dac-news-page) .devsite-main-content:not([has-toc]) devsite-dynamic-content .devsite-card-image-bg{height:170px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding-bottom:28.125%}}@media screen and (max-width:720px){devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author{padding-left:64px}devsite-dynamic-content .devsite-card[dynamic-card-type=android\ developers] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=blogger] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=medium] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=video] .devsite-card-author:before,devsite-dynamic-content .devsite-card[dynamic-card-type=youtube] .devsite-card-author:before{left:24px}[has-toc] devsite-dynamic-content .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{margin-bottom:0;font:500 14px/22px Roboto Mono,monospace;letter-spacing:1.5px}@media screen and (max-width:720px){.dac-label .devsite-landing-row-item-labels a,.dac-label .devsite-landing-row-item-labels span{font:500 12px/22px Roboto Mono,monospace}}.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-blue .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-green .devsite-landing-row-item-labels span,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels a,.dac-label.dac-landing-row-bg-slate .devsite-landing-row-item-labels span{color:#fff}.dac-label.dac-success-story .devsite-landing-row-item-labels a,.dac-label.dac-success-story .devsite-landing-row-item-labels span{margin-bottom:16px}.dac-label.devsite-landing-row-item h3:first-child,.dac-label.devsite-landing-row h3:first-child,.dac-label h3{margin-top:16px}.dac-featured-cards{margin-bottom:32px}.dac-featured-cards .devsite-landing-row-column{background:#f7f9fa;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-width:0}[layout=docs] .dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-featured-cards a{color:inherit}.dac-featured-cards .devsite-landing-row-column h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-featured-cards .devsite-landing-row-column img{display:block;margin:0 auto 16px;max-width:150px}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{margin-top:0;width:60%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons,[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-top:0}[has-book-nav] .dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item-description{padding:24px}}[has-book-nav] .dac-featured-cards .devsite-landing-row-item-buttons .dac-button{white-space:inherit}@media screen and (max-width:1400px){.dac-featured-cards .devsite-landing-row-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type{width:100%}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:first-of-type .devsite-landing-row-item-buttons{display:none}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2) .devsite-landing-row-item-buttons{display:block}.dac-featured-cards .devsite-landing-row-column .devsite-landing-row-item:nth-of-type(2).devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}.dac-featured-cards .devsite-landing-row-item:last-child .devsite-landing-row-item-buttons{padding-top:0}}.dac-get-involved.dac-center{width:auto}.dac-footer-promos{font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}.dac-footer-promos .devsite-footer-promos-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:20px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}.dac-footer-promos .devsite-footer-promo-icon{display:block;height:48px;margin:0 auto 8px;width:48px}.dac-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}.dac-footer-promos .devsite-footer-promo-title:focus,.dac-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8}@media screen and (max-width:840px){.dac-footer-promos{padding:0}.dac-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.dac-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}.dac-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}.dac-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}.dac-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}.dac-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){.dac-footer-promos .devsite-footer-promos-list{display:block}}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3,.devsite-landing-row h1,.devsite-landing-row h2{color:#414141}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-landing-row-hero h2,.devsite-landing-row-item h2,.devsite-landing-row-item h3{letter-spacing:-.5px}.dac-heading-medium.devsite-landing-row-item h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-subtitle :first-child{margin-top:0}.dac-subtitle :last-child{margin-bottom:0}@media screen and (max-width:720px){.dac-subtitle{margin-bottom:32px;margin-top:32px}}@media screen and (max-width:1200px){.dac-heading-small.devsite-landing-row-item h3,.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:48px;margin-top:88px}}@media screen and (max-width:1000px){.devsite-landing-row-item .dac-subtitle h3,.devsite-landing-row-item h3{margin-bottom:26px}.devsite-landing-row-item h2,.devsite-landing-row-item h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-large.devsite-landing-row-item h3,.dac-landing-row-hero h1,.dac-landing-row-hero h2,.dac-landing-row h1,.dac-sublandings-hero h1,.dac-subtitle h3{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-heading-medium.devsite-landing-row-item h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subtitle{margin-bottom:32px;margin-top:48px}}.dac-heading-logo{padding:24px 0 8px}.dac-security-page [background]:not([background=grey]) :link :not(.material-icons):hover{text-decoration:none}.dac-heading-icon,.dac-heading-icon-small,.dac-heading-icon-tiny{display:block;margin-bottom:24px}.dac-heading-icon{height:80px}.dac-heading-icon.material-icons{font-size:80px}.dac-heading-icon-small{height:62px}.dac-heading-icon-small.material-icons{font-size:62px}.dac-heading-icon-tiny{height:40px}.dac-heading-icon-tiny.material-icons{font-size:40px}.dac-landing-row-hero{color:#414141;margin:84px 0;text-align:center}@media screen and (max-width:1200px){.dac-landing-row-hero{margin:60px 0}}@media screen and (max-width:1000px){.dac-landing-row-hero{margin:48px 0}}@media screen and (max-width:720px){.dac-landing-row-hero{margin:36px 0}}[layout=docs] .dac-landing-row-hero{margin:44px 0}@media screen and (max-width:1000px){[layout=docs] .dac-landing-row-hero{margin:36px 0}}.dac-landing-row-hero-screenshot{margin-top:72px}@media screen and (max-width:720px){.dac-landing-row-hero-screenshot{margin-top:48px}}.dac-landing-row-hero-screenshot img{-webkit-box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15);box-shadow:0 2px 3px 0 rgba(60,64,67,.3),0 6px 10px 4px rgba(60,64,67,.15)}.dac-landing-row-hero-description{color:#263238;font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:24px auto;max-width:800px}.dac-landing-hero{text-align:center}.dac-landing-hero .devsite-landing-row-header{display:block}.dac-landing-hero .devsite-landing-row-header-buttons{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:52px}.dac-landing-hero h1{color:inherit}.dac-sub-hero{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (max-width:1000px){.dac-sub-hero{display:block;margin-top:24px}}@media screen and (max-width:720px){.dac-sub-hero{margin-top:16px}}.dac-sub-hero h1{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:20px}@media screen and (max-width:1000px){.dac-sub-hero h1{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-sub-hero-image{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0 0 16px 16px;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;text-align:center}.dac-sub-hero-image img{max-height:310px}@media screen and (max-width:1000px){.dac-sub-hero-image img{max-height:150px}}.dac-sub-hero-copy{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description[icon-position=left]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:32px}@media screen and (max-width:720px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-body{padding:24px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{-webkit-box-flex:1;-webkit-flex:1 0 200px;flex:1 0 200px}@media screen and (max-width:1000px){.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child{height:200px}}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>.devsite-landing-row-item-icon-container:hover,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:focus,.dac-landing-row-item-icon-container-left .devsite-landing-row-item-description>a:first-child:hover{opacity:.8}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon{height:auto;margin:0;width:95px}.dac-landing-row-item-icon-container-left .devsite-landing-row-item-icon-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:100%;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-landing-row-item-icon-container-bg-yellow .devsite-landing-row-item-icon-container{background-color:#ffd600}.dac-landing-row-item-icon-container-bg-green .devsite-landing-row-item-icon-container{background-color:#00e676}.dac-landing-row-item-icon-container-bg-red .devsite-landing-row-item-icon-container{background-color:#ff5252}.dac-landing-row-item-icon-container-bg-blue .devsite-landing-row-item-icon-container{background-color:#2196f3}.dac-chevron-right:before{content:"arrow_forward";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;vertical-align:middle}.dac-chevron-right:focus{text-decoration:none}.dac-landing-row-item-buttons-right .devsite-landing-row-item-buttons{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-landing-row-item-description-bg-white .devsite-landing-row-item-description{background-color:#fff}.dac-landing-row-item-description-bg-transparent .devsite-landing-row-item-description{background-color:none}.dac-home-page .dac-gservices .dac-banner-card-bg-img-item{background:url(../images/custom/home-google-services-for-android.svg) 50%/calc(100% - 32px) no-repeat #f7f9fa}.dac-home-design-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/home-design-illustration.svg)}.dac-about-hero-illustration{background:url(../images/custom/camerax.png) 0/auto 125% no-repeat #f7f9fa}.dac-things-page .dac-banner-card.devsite-landing-row .dac-banner-card-bg-img-item.dac-banner-card-bg-img-contain{background:url(../images/custom/prototype-to-production.svg) no-repeat 0/900px #455a64}.dac-play-page .dac-play-hero-banner:after{background:url(../images/custom/google-play-policies.png) 85%/75% no-repeat}.dac-landing-row-bg-illustration-2:after{background-image:url(../images/custom/keyline-illustration-2.svg)}.dac-landing-row-bg-blob-5{background:url(../images/custom/blob-illustration-5.svg) bottom/cover no-repeat #455a64}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 280px) -webkit-calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 280px) calc(100% + 150px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-8{background:url(../images/custom/blob-illustration-8.svg) -webkit-calc(100% + 80px) -webkit-calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-8.svg) calc(100% + 80px) calc(100% + 100px)/contain no-repeat content-box content-box #f7f9fa}.dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:16px 0;text-align:center}.dac-illustration-block img{display:inline-block;max-width:100%}.dac-landing-row-illustration-kotlin .dac-illustration-block img{margin-top:40px}.dac-illustration-block-edge{margin:0 -32px;width:-webkit-calc(100% + 64px);width:calc(100% + 64px)}.dac-illustration-block-edge-left .dac-illustration-block-edge-right{width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}.dac-illustration-block-edge-left{margin-left:-32px}.dac-illustration-block-edge-right{margin-right:-32px}.dac-illustration-block-edge-left img,.dac-illustration-block-edge-right img,.dac-illustration-block-edge img{width:100%}.dac-illustration{position:relative}.dac-illustration:after{background-position:top;-o-background-size:600px auto;background-size:600px auto;content:"";display:block;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;min-height:250px;-webkit-box-ordinal-group:6;-webkit-order:5;order:5;width:100%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-illustration .devsite-landing-row-item-description{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-40px;width:-webkit-calc(100% + 40px);width:calc(100% + 40px)}@media screen and (max-width:1000px){.dac-illustration-bleed-lb:after{margin-bottom:-40px;margin-left:-16px;width:-webkit-calc(100% + 32px);width:calc(100% + 32px)}}.dac-illustration-keyline-1:after{background-image:url(../images/custom/keyline-illustration-1.svg)}.dac-illustration-keyline-3:after{background-image:url(../images/custom/keyline-illustration-3.svg)}.dac-illustration-keyline-4:after{background-image:url(../images/custom/keyline-illustration-4.svg)}.dac-illustration-keyline-5:after{background-image:url(../images/custom/keyline-illustration-5.svg)}.dac-illustration-keyline-3:after,.dac-illustration-keyline-4:after,.dac-illustration-keyline-5:after{-o-background-size:500px auto;background-size:500px auto}.dac-illustration-play-book:after{background:url(../images/custom/playbook-illustration-2.svg) 100% 0 no-repeat content-box}.dac-illustration-auto:after{background:url(../images/custom/auto-illustration.svg) 100% 100%/auto 100% no-repeat}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob.svg) 160px 130%/auto no-repeat content-box #455a64}.dac-illustration-kotlin-bootcamp:after{background:url(../images/custom/kotlin-bootcamp.png) 50%/cover no-repeat}.dac-illustration-guides-policies-families:after{background:url(../images/custom/google-play-guides-families-blob-2.svg) 0 30px/auto 135% no-repeat;width:35%}.dac-illustration-guides-go:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) right 35px/auto 100% no-repeat}.dac-illustration-guides-games:after{background:url(../images/custom/google-play-guides-games.png) center right 15px/auto 65% no-repeat}.dac-illustration-guides-families:after{background:url(../images/custom/google-play-guides-families-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-indie:after{background:url(../images/custom/google-play-guides-indie-games-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-start:after{background:url(../images/custom/google-play-guides-start-on-android-simplified.svg) 100%/auto 75% no-repeat}.dac-illustration-guides-subscriptions:after{background:url(../images/custom/google-play-guides-subscriptions-simplified.svg) right 35px/auto 95% no-repeat}.dac-illustration-guides:after{background:url(../images/custom/google-play-guides-android-go-edition.svg) 150% 0/auto 110% no-repeat}.dac-illustration-play-console:after{background:url(../images/custom/floating-playtime-elements-mobile.svg) left 0/cover no-repeat,url(../images/custom/playtime-hero-bg-fluid-mobile.svg) 0 50px/180% no-repeat;min-height:360px}.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 75%/30% no-repeat,#f7f9fa}@media screen and (max-width:1200px){.dac-illustration-newsletter-archive{background:url(../images/custom/newsletter-archive-banner.svg) 80%/40% no-repeat,#f7f9fa}}@media screen and (max-width:720px){.dac-illustration-newsletter-archive{background:#f7f9fa}}.dac-landing-row-bg-illustration-3{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-landing-row-bg-illustration-3 .devsite-landing-row-item-description-content p{max-width:100%}.dac-landing-row-bg-illustration-3:after{background:url(../images/custom/success-guide-illustration-1.svg) 0 no-repeat content-box;height:100%;margin-left:-1em;width:100%}.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-illustration-column .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:50%}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-illustration-column:after{height:100%;width:50%}.dac-illustration-column{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after{min-height:500px}.dac-about-hero-illustration .dac-media img{display:none}.dac-play-policies-page .dac-illustration-android-q-ready:after{background:url(../images/custom/play-policies-q-logo.png) 50%/contain no-repeat}.dac-play-policies-page .dac-illustration-play-policies-rating:after{background:url(../images/custom/play-policies-rating.svg) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-icon:after{background:url(../images/custom/play-policies-icon.png) 50%/70% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-audience:after{background:url(../images/custom/play-policies-target-audience.png) 50%/45% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-64-bit:after{background:url(../images/custom/play-policies-64-bit.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-target-api:after{background:url(../images/custom/play-policies-target-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-developer-api:after{background:url(../images/custom/play-policies-dev-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-billing-library:after{background:url(../images/custom/play-policies-billing-library.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-comply:after{background:url(../images/custom/play-policies-comply.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-monetization:after{background:url(../images/custom/play-policies-monetization.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-referrer-api:after{background:url(../images/custom/play-policies-referrer-api.png) 50%/50% no-repeat}.dac-play-policies-page .dac-illustration-play-policies-workmanager:after{background:url(../images/custom/play-policies-workmanager.png) 50%/50% no-repeat}.devsite-404-header.devsite-header-billboard:before{background:url(../images/custom/404-illustration-question-mark.png) 50%/contain no-repeat}.devsite-404-header.devsite-header-billboard:after{background:url(../images/custom/404-illustration-telescope.png) 50%/contain no-repeat}@media screen and (max-width:1200px){.dac-play-page .dac-play-hero-banner:after{background-position:95%;-o-background-size:90%;background-size:90%}}@media screen and (max-width:1000px){.dac-about-hero-illustration{-o-background-size:auto 94%;background-size:auto 94%}.dac-illustration-column .devsite-landing-row-item-description,.dac-illustration-column:after{width:100%}.dac-landing-row-bg-blob-7{background:url(../images/custom/blob-illustration-7.svg) -webkit-calc(100% + 250px) -webkit-calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa;background:url(../images/custom/blob-illustration-7.svg) calc(100% + 250px) calc(100% + 119px)/contain no-repeat content-box content-box #f7f9fa}.dac-landing-row-bg-blob-3:after{background-image:url(../images/custom/blob-illustration-3-large.svg);background-position:0 0;padding-left:30%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-landing-row-bg-illustration-2 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-illustration-2:after,.dac-play-console-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-console-page .dac-illustration-play-console:after,.dac-play-page .dac-illustration-play-console .devsite-landing-row-item-description,.dac-play-page .dac-illustration-play-console:after{width:auto}.dac-landing-row-illustration-kotlin{background:url(../images/custom/kotlin-blob-mobile.svg) bottom/100% no-repeat content-box #455a64}.dac-landing-row-bg-blob-3:after{background:url(../images/custom/blob-illustration-3-small.svg) bottom/cover content-box content-box no-repeat #fff;min-height:425px;padding-left:0;width:100%}.dac-landing-row-bg-blob-1{background-position:85% 100%}.dac-landing-row-bg-blob-7{background-position:-webkit-calc(100% + 417px) -webkit-calc(100% + 210px);background-position:calc(100% + 417px) calc(100% + 210px);-o-background-size:750px;background-size:750px}.dac-illustration-block img{max-width:60%}.devsite-landing-page-with-side-navs .dac-illustration{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.devsite-landing-page-with-side-navs .dac-illustration:after{min-height:300px}.dac-illustration-auto:after{-o-background-size:cover;background-size:cover}.dac-illustration-play-console{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-illustration:after{min-height:250px}.dac-illustration-play-console:after{background-clip:content-box;background-image:url(../images/custom/floating-playtime-elements.svg),url(../images/custom/playtime-hero-bg-fluid.svg)}.dac-illustration-block-edge{margin:0;width:auto}}@media screen and (max-width:600px){.dac-play-page .dac-play-hero-banner:after{background-position:63%;-o-background-size:80%;background-size:80%}.dac-about-hero-illustration .dac-media img{display:block}.dac-about-hero-illustration{background-image:none}.dac-about-hero-illustration .dac-full-width-media{margin-top:0!important}.dac-illustration-guides-families:after,.dac-illustration-guides-go:after,.dac-illustration-guides-indie:after,.dac-illustration-guides-start:after,.dac-illustration-guides-subscriptions:after,.dac-illustration-guides:after{background-position:50%}.dac-illustration-guides-policies-families:after{background:url(../images/custom/blob-illustration-3-small.svg) -webkit-calc(100% + 150px) -webkit-calc(100% + 165px)/auto 170% no-repeat;background:url(../images/custom/blob-illustration-3-small.svg) calc(100% + 150px) calc(100% + 165px)/auto 170% no-repeat;width:100%}}ol.callouts{counter-reset:item;list-style-type:none;margin-left:30px;padding-left:0}ol.callouts>li:before{background:url(../images/custom/callout-bg_2x.png) 1px 2px/20px no-repeat;-webkit-box-sizing:content-box;box-sizing:content-box;content:counter(item);counter-increment:item;margin-left:-30px;padding-left:8px;position:absolute;width:16px}.callout{background:url(../images/custom/callout-bg_2x.png) 0 2px/20px no-repeat;display:inline-block;height:22px;text-align:center;width:20px}.callout,ol.callouts>li:before{color:#fff;font-size:12px;font-weight:500}.img-caption,figcaption{font-size:14px;margin-top:-4px}p+.img-caption{margin-top:-20px}li p+.img-caption{margin-top:-12px}.table-caption{clear:both;font-size:14px;margin:0 0 4px}.table-caption+.devsite-table-wrapper,.table-caption+table{margin-top:0}.code-caption{font:14px/1.5 monospace;margin-bottom:4px}a.external-link:after{content:"open_in_new";font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;word-wrap:normal;font-size:18px;margin-left:4px;vertical-align:text-bottom}.sidebox{background:#f1f3f4;padding:12px 24px}.sidebox a.bug{padding-right:25px;background:transparent url(../images/custom/bug.png) no-repeat 100%}.sidebox a.g-plus{padding-right:25px;background:transparent url(../images/custom/g+.ico) no-repeat 100%}.sidebox a.codelab{padding-right:40px;background:transparent url(../images/custom/favicon.png) no-repeat 100%}.sidebox p{margin-top:0}.wrap{margin:0 auto;clear:both}.cols{margin-left:-10px;margin-right:-10px}.cols:after,.cols:before{content:" ";display:table}.cols:after{clear:both}[class*=col-]{-webkit-box-sizing:border-box;box-sizing:border-box;float:left;min-height:1px;padding-left:10px;padding-right:10px;position:relative}.col-1{width:6.25%}.col-2{width:12.5%}.col-3{width:18.75%}.col-4{width:25%}.col-5{width:31.25%}.col-6{width:37.5%}.col-7{width:43.75%}.col-8{width:50%}.col-9{width:56.25%}.col-10{width:62.5%}.col-11{width:68.75%}.col-12{width:75%}.col-13{width:81.25%}.col-14{width:87.5%}.col-15{width:93.75%}.col-16{width:100%}.col-13 .col-1{width:7.69230769%}.col-13 .col-2{width:15.38461538%}.col-13 .col-3{width:23.07692308%}.col-13 .col-4{width:30.76923077%}.col-13 .col-5{width:38.46153846%}.col-13 .col-6{width:46.15384615%}.col-13 .col-7{width:53.84615385%}.col-13 .col-8{width:61.53846154%}.col-13 .col-9{width:69.23076923%}.col-13 .col-10{width:76.92307692%}.col-13 .col-11{width:84.61538462%}.col-13 .col-12{width:92.30769231%}.col-13 .col-13{width:100%}.col-12 .col-1{width:8.33333333%}.col-12 .col-2{width:16.66666667%}.col-12 .col-3{width:25%}.col-12 .col-4{width:33.33333333%}.col-12 .col-5{width:41.66666667%}.col-12 .col-6{width:50%}.col-12 .col-7{width:58.33333333%}.col-12 .col-8{width:66.66666667%}.col-12 .col-9{width:75%}.col-12 .col-10{width:83.33333333%}.col-12 .col-11{width:91.66666667%}.col-1of1,.col-2of2,.col-3of3,.col-4of4,.col-5of5,.col-6of6,.col-8of8,.col-10of10,.col-12 .col-12,.col-12of12,.col-16of16{width:100%}.col-1of2,.col-2of4,.col-3of6,.col-4of8,.col-5of10,.col-6of12,.col-8of16{width:50%}.col-1of3,.col-2of6,.col-4of12{width:33.33333333%}.col-2of3,.col-4of6,.col-8of12{width:66.66666667%}.col-1of4,.col-2of8,.col-3of12,.col-4of16{width:25%}.col-3of4,.col-6of8,.col-9of12,.col-12of16{width:75%}.col-1of5,.col-2of10{width:20%}.col-2of5,.col-4of10{width:40%}.col-3of5,.col-6of10{width:60%}.col-4of5,.col-8of10{width:80%}.col-1of6,.col-2of12{width:16.66666667%}.col-5of6,.col-10of12{width:83.33333333%}.col-1of8,.col-2of16{width:12.5%}.col-3of8,.col-6of16{width:37.5%}.col-5of8,.col-10of16{width:62.5%}.col-7of8,.col-14of16{width:87.5%}.col-1of10{width:10%}.col-3of10{width:30%}.col-7of10{width:70%}.col-9of10{width:90%}.col-1of12{width:8.33333333%}.col-5of12{width:41.66666667%}.col-7of12{width:58.33333333%}.col-11of12{width:91.66666667%}.col-1of16{width:6.25%}.col-3of16{width:18.75%}.col-5of16{width:31.25%}.col-7of16{width:43.75%}.col-9of16{width:56.25%}.col-11of16{width:68.75%}.col-13of16{width:81.25%}.col-15of16{width:93.75%}.col-pull-1of1,.col-pull-2of2,.col-pull-3of3,.col-pull-4of4,.col-pull-5of5,.col-pull-6of6,.col-pull-8of8,.col-pull-10of10,.col-pull-12of12,.col-pull-16of16{left:-100%}.col-pull-1of2,.col-pull-2of4,.col-pull-3of6,.col-pull-4of8,.col-pull-5of10,.col-pull-6of12,.col-pull-8of16{left:-50%}.col-pull-1of3,.col-pull-2of6,.col-pull-4of12{left:-33.33333333%}.col-pull-2of3,.col-pull-4of6,.col-pull-8of12{left:-66.66666667%}.col-pull-1of4,.col-pull-2of8,.col-pull-3of12,.col-pull-4of16{left:-25%}.col-pull-3of4,.col-pull-6of8,.col-pull-9of12,.col-pull-12of16{left:-75%}.col-pull-1of5,.col-pull-2of10{left:-20%}.col-pull-2of5,.col-pull-4of10{left:-40%}.col-pull-3of5,.col-pull-6of10{left:-60%}.col-pull-4of5,.col-pull-8of10{left:-80%}.col-pull-1of6,.col-pull-2of12{left:-16.66666667%}.col-pull-5of6,.col-pull-10of12{left:-83.33333333%}.col-pull-1of8,.col-pull-2of16{left:-12.5%}.col-pull-3of8,.col-pull-6of16{left:-37.5%}.col-pull-5of8,.col-pull-10of16{left:-62.5%}.col-pull-7of8,.col-pull-14of16{left:-87.5%}.col-pull-1of10{left:-10%}.col-pull-3of10{left:-30%}.col-pull-7of10{left:-70%}.col-pull-9of10{left:-90%}.col-pull-1of12{left:-8.33333333%}.col-pull-5of12{left:-41.66666667%}.col-pull-7of12{left:-58.33333333%}.col-pull-11of12{left:-91.66666667%}.col-pull-1of16{left:-6.25%}.col-pull-3of16{left:-18.75%}.col-pull-5of16{left:-31.25%}.col-pull-7of16{left:-43.75%}.col-pull-9of16{left:-56.25%}.col-pull-11of16{left:-68.75%}.col-pull-13of16{left:-81.25%}.col-pull-15of16{left:-93.75%}.col-push-1of1,.col-push-2of2,.col-push-3of3,.col-push-4of4,.col-push-5of5,.col-push-6of6,.col-push-8of8,.col-push-10of10,.col-push-12of12,.col-push-16of16{left:100%}.col-push-1of2,.col-push-2of4,.col-push-3of6,.col-push-4of8,.col-push-5of10,.col-push-6of12,.col-push-8of16{left:50%}.col-push-1of3,.col-push-2of6,.col-push-4of12{left:33.33333333%}.col-push-2of3,.col-push-4of6,.col-push-8of12{left:66.66666667%}.col-push-1of4,.col-push-2of8,.col-push-3of12,.col-push-4of16{left:25%}.col-push-3of4,.col-push-6of8,.col-push-9of12,.col-push-12of16{left:75%}.col-push-1of5,.col-push-2of10{left:20%}.col-push-2of5,.col-push-4of10{left:40%}.col-push-3of5,.col-push-6of10{left:60%}.col-push-4of5,.col-push-8of10{left:80%}.col-push-1of6,.col-push-2of12{left:16.66666667%}.col-push-5of6,.col-push-10of12{left:83.33333333%}.col-push-1of8,.col-push-2of16{left:12.5%}.col-push-3of8,.col-push-6of16{left:37.5%}.col-push-5of8,.col-push-10of16{left:62.5%}.col-push-7of8,.col-push-14of16{left:87.5%}.col-push-1of10{left:10%}.col-push-3of10{left:30%}.col-push-7of10{left:70%}.col-push-9of10{left:90%}.col-push-1of12{left:8.33333333%}.col-push-5of12{left:41.66666667%}.col-push-7of12{left:58.33333333%}.col-push-11of12{left:91.66666667%}.col-push-1of16{left:6.25%}.col-push-3of16{left:18.75%}.col-push-5of16{left:31.25%}.col-push-7of16{left:43.75%}.col-push-9of16{left:56.25%}.col-push-11of16{left:68.75%}.col-push-13of16{left:81.25%}.col-push-15of16{left:93.75%}.dac-row-logos{padding:0}.dac-row-logos .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-row-logos .devsite-landing-row-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 5px}.dac-row-logos .devsite-landing-row-item-description{display:none}.dac-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-landing-row-media .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-full-width-media .devsite-landing-row-item-body,.dac-media{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-media{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-self:flex-end;align-self:flex-end;-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;width:100%}.dac-media,.dac-media-video{display:-webkit-box;display:-webkit-flex;display:flex}.dac-media-video{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start}.dac-media-video-toolbar{height:auto;margin-left:3%;width:15%}.dac-full-width-media[background],.dac-full-width-media[background].dac-grow-7.devsite-landing-row-item-no-media:not(:first-child){margin-left:0}@media screen and (max-width:1000px){.dac-media{margin-top:8px}.devsite-landing-row:not(.devsite-landing-row-logos) .dac-full-width-media .devsite-landing-row-item-description{padding-top:0}}[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:720px){[class*=dac-landing-row-bg] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row-item[background] .devsite-landing-row-item-description,[class^=dac] .devsite-landing-row[background] .devsite-landing-row-item-description{padding:24px}}.dac-landing-row-item-icon-container-left.devsite-landing-row-item .devsite-landing-row-item-description{padding:0}.dac-top-spacing{padding-top:120px}.dac-bottom-spacing{padding-bottom:88px}@media screen and (max-width:720px){.dac-top-spacing{padding-top:64px}.dac-bottom-spacing{padding-bottom:40px}}.dac-success-story{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content{-webkit-box-pack:end;-webkit-justify-content:end;justify-content:end}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-description-content p strong{padding-left:4px}.dac-success-story h4{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 16px}.dac-success-story .devsite-landing-row-item-description-content{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.dac-success-story .devsite-landing-row-item-media{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;padding:24px}.dac-success-story:not(.dac-testimonial) .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0}.dac-success-story img{width:128px}.dac-success-story .devsite-landing-row-item-image{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:transparent;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.dac-success-story .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0}@media screen and (max-width:720px){.dac-success-story{display:block}.dac-success-story .devsite-landing-row-item-media{padding:24px 24px 0}.dac-success-story .devsite-landing-row-item-image{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-success-story.devsite-landing-row-item .devsite-landing-row-item-media{margin:0}}.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{content:"";display:block;position:absolute}.devsite-404-header.devsite-header-billboard:before{bottom:-210px;height:428px;left:-35%;width:321px;z-index:1}.devsite-404-header.devsite-header-billboard:after{bottom:-297px;height:508px;left:86%;width:391px;z-index:3}.devsite-404-search{z-index:2}.devsite-404-search devsite-search .devsite-searchbox:before{background:0}.devsite-404-header,.devsite-offline-header{z-index:auto}@media screen and (max-width:1200px){.devsite-404-header.devsite-header-billboard:before{bottom:-180px;left:-21%;width:271px}.devsite-404-header.devsite-header-billboard:after{bottom:-350px;left:74%;width:321px;z-index:2}}@media screen and (max-width:1000px){.devsite-404-header.devsite-header-billboard:after,.devsite-404-header.devsite-header-billboard:before{background:0}}.dac-app-bundle-page h2{color:inherit}.dac-app-bundle-page .dac-logos,.dac-app-bundle-page .devsite-landing-row-header{text-align:center}.dac-app-bundle-page .dac-landing-row-hero .dac-landing-row-hero-description{margin-top:64px}.dac-app-bundle-page .dac-featured-cards .devsite-landing-row-item-description-content>p>a,.dac-app-bundle-page .dac-logos a{color:#1a73e8}.dac-app-bundle-page .dac-smaller-app img{max-width:100%}.dac-app-bundle-page .dac-list-wrapped{padding-bottom:0}.dac-app-bundle-page .dac-list-wrapped h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){.dac-app-bundle-page .dac-list-wrapped h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-app-bundle-page .dac-list-wrapped .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;padding-top:40px}.dac-app-bundle-page .dac-app-featured-cards{margin-bottom:0}.dac-app-bundle-page .dac-logos h2{margin-top:120px}.dac-app-bundle-page .dac-logos .devsite-landing-row-item-description{padding:32px 0}.dac-app-bundle-page .dac-logos img{margin:0 auto;max-height:80px;max-width:80px}.dac-app-bundle-page .dac-logos h4,.dac-app-bundle-page .dac-logos p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;white-space:nowrap}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group{-webkit-box-pack:space-evenly;-webkit-justify-content:space-evenly;justify-content:space-evenly}.dac-app-bundle-page .dac-logos.dac-bottom-logos .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:0 0 80px;flex:0 0 80px}.dac-app-bundle-page .dac-get-started{background:url(../images/custom/platform-app-bundle-blob.svg) left -webkit-calc(50% + 300px) bottom -250px/500px no-repeat;background:url(../images/custom/platform-app-bundle-blob.svg) left calc(50% + 300px) bottom -250px/500px no-repeat;margin-top:64px}.dac-app-bundle-page .dac-get-started .devsite-landing-row-item{padding:0 32px 32px}.devsite-landing-row-column>.dac-grow-1.devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:first-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:last-child{-webkit-box-flex:4;-webkit-flex-grow:4;flex-grow:4}.dac-docs-overview-page .dac-grow-column-4-8 .devsite-landing-row-column:last-child,.dac-docs-overview-page .dac-grow-column-4-8.dac-column-switch .devsite-landing-row-column:first-child{-webkit-box-flex:8;-webkit-flex-grow:8;flex-grow:8}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-docs-overview-page .dac-column-switch.devsite-landing-row .devsite-landing-row-column:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-docs-overview-page .dac-guides-group{-webkit-column-break-inside:avoid;break-inside:avoid;-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;margin-bottom:16px}.dac-docs-overview-page .dac-guides-flex-columns .dac-guides-group{padding-left:0}.dac-docs-overview-page .dac-guides-group h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}.dac-docs-overview-page .dac-landing-row-bg-blob-5,.dac-docs-overview-page .dac-landing-row-bg-blue{min-width:280px}.dac-docs-overview-page .dac-grow-column-4-8 .dac-guides-flex-columns-xs,.dac-docs-overview-page .dac-illustration-keyline-1,.dac-docs-overview-page .dac-illustration-keyline-3{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-heading-small h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-bottom:26px}.dac-guides-flex-columns .dac-guides{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-guides-flex-columns .dac-guides-group{-webkit-box-flex:0;-webkit-flex:0 0 33%;flex:0 0 33%;padding:0 16px}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description,.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content,.dac-docs-overview-page .dac-landing-row-item .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-docs-overview-page .dac-landing-row-bg-blob-5 .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-docs-overview-page .dac-center .dac-samples-form select{margin-left:16px}.dac-docs-overview-page .dac-center .dac-samples-form .dac-button{margin:8px 16px}.dac-docs-overview-page .dac-subtitle{margin-bottom:48px;margin-top:88px}.dac-docs-overview-page .devsite-landing-row[background=grey] .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}@media screen and (max-width:1200px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:50%;flex-basis:50%;max-width:50%}}@media screen and (max-width:1000px){.dac-guides-flex-columns .dac-guides-group{-webkit-flex-basis:100%;flex-basis:100%;max-width:100%}}@media screen and (max-width:720px){.dac-docs-overview-page .dac-subtitle{margin-bottom:16px;margin-top:16px}.dac-docs-overview-page .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-docs-overwiew-page .dac-landing-row-hero{margin-bottom:0}.dac-docs-overview-page .devsite-landing-row-2-up:not(.devsite-landing-row-logos) .devsite-landing-row-column{margin-left:0;margin-top:40px;width:100%}.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-docs-overview-page .devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child).dac-landing-row-bg-blob-5{margin:40px 0 0}.dac-docs-overview-page .dac-column-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-column:not(:last-child){margin-left:0}}.dac-full-width-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-full-width-page h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page h2{font:300 44px/56px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-full-width-page h2{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-full-width-page .dac-sub-hero{margin-top:50px}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{-webkit-box-flex:2;-webkit-flex-grow:2;flex-grow:2}}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image{margin:0}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px 32px;max-width:360px;width:100%}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-image img{margin:0 0 40px}}@media screen and (min-width:1000px) and (max-width:1400px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-copy{-webkit-box-flex:3;-webkit-flex-grow:3;flex-grow:3}}.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-top:64px}@media screen and (max-width:1000px){.dac-full-width-page .dac-sub-hero .dac-sub-hero-buttons{display:block}}.dac-full-width-page .dac-features .devsite-landing-row-item-description{padding:0 0 0 96px}.dac-full-width-page .devsite-landing-row-item{position:relative}.dac-full-width-page .dac-features img{height:auto;left:0;max-width:64px;position:absolute;top:0}.dac-full-width-page .dac-components h4,.dac-full-width-page .dac-features h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-full-width-page .dac-components .devsite-landing-row-item-image{background:0;max-width:320px}.dac-full-width-page .dac-components ul{list-style:none;padding-left:0}.dac-full-width-page .dac-components p{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-full-width-page .dac-components .dac-button{margin:16px auto 16px 0}.dac-full-width-page.devsite-landing-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px)}.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;width:auto}@media screen and (max-width:1000px){.dac-full-width-page .dac-success-story .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center}}.dac-full-width-page .devsite-landing-row-logos .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;text-align:center}.dac-full-width-page .devsite-landing-row-logos img{max-height:100px;max-width:200px}body[theme=android-theme] devsite-header .devsite-header-billboard h1{color:#414141;font:400 44px/54px Euclid,sans-serif;margin-top:0;text-align:center}body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field,body[theme=android-theme] devsite-header .devsite-header-billboard-search devsite-search .devsite-search-field:hover{background-color:#fff}.dac-home-page .dac-outline-button{border-color:#414141;color:#414141}.dac-home-page .dac-outline-button:focus,.dac-home-page .dac-outline-button:hover{background:#414141;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button{border-color:#fff;color:#fff}.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:focus,.dac-home-page .dac-landing-row-bg-slate .dac-outline-button:hover{background:#fff;color:#414141}.dac-home-page .dac-alt-flat-button{color:#414141}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:64px 40px}.dac-home-hero-banner-text{max-width:630px}.dac-home-hero-banner-text .devsite-landing-row-item-description-content{margin-bottom:16px}.dac-home-hero-banner-image{text-align:center}.dac-home-hero-banner-image img{max-width:450px}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:none}.dac-home-page .dac-feature-card-illustration{max-width:200px}.dac-home-page .dac-featured-cards{margin-bottom:112px}.dac-home-build-an-app{padding-top:112px}.dac-home-developer-guides{padding-bottom:112px}.dac-home-developer-guides-heading h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-no-spacing .devsite-landing-row-item-description{padding:0!important}.dac-home-developer-guides img{display:block;margin-bottom:32px;width:80px}.dac-home-design-card h3{margin-bottom:24px}.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:100px}.dac-home-platforms img{display:block;margin:40px auto;max-width:312px}.dac-gservices,.dac-gservices img{margin-top:32px}.dac-gservices .devsite-landing-row-item-buttons{margin-top:200px}.dac-gservices p:last-of-type{margin:0}@media screen and (max-width:1200px){.dac-home-developer-guides .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-20px}.dac-home-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item{-webkit-flex-basis:-webkit-calc((100% - 80px)/2);flex-basis:calc((100% - 80px)/2);margin:20px}.dac-home-developer-guides-heading h3{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 40px/48px Euclid,sans-serif}.dac-home-build-an-app{padding-top:64px}.dac-home-developer-guides{padding-bottom:64px}.dac-home-developer-guides-heading h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:720px){body[theme=android-theme] devsite-header .devsite-header-billboard h1{font:400 32px/40px Euclid,sans-serif}.dac-home-page .dac-featured-cards{margin-bottom:64px}.dac-home-build-an-app{padding-top:32px}.dac-home-hero-banner .devsite-landing-row-group{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-home-hero-banner-image{margin:0!important;width:100%}.dac-home-hero-banner-image .devsite-landing-row-item-buttons{display:block;text-align:left}.dac-home-hero-banner-text .devsite-landing-row-item-buttons{display:none}.dac-home-developer-guides{padding-bottom:32px}.dac-gservices .devsite-landing-row-item-buttons,.dac-home-design-card .devsite-landing-row-item-buttons{margin-top:24px}}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-jetpack-page .dac-cta.dac-center .devsite-landing-row-item-buttons{display:block}.dac-jetpack-page .dac-component h3 p{font-weight:400;height:130px}.dac-jetpack-page .dac-component button{display:none}.dac-jetpack-page .devsite-landing-row-logos img{padding:12px}.dac-jetpack-page .dac-testimonial a,.dac-jetpack-page .dac-testimonial a:link,.dac-jetpack-page .dac-testimonial a:visited{color:inherit;font-weight:700}@media screen and (max-width:1200px){.dac-jetpack-page .dac-components .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-jetpack-page .dac-components .dac-component{-webkit-flex-basis:-webkit-calc((100% - 40px)/2);flex-basis:calc((100% - 40px)/2)}.dac-jetpack-page .dac-components.devsite-landing-row .devsite-landing-row-item:not(:first-child){margin:40px 0 0}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2){margin:0 0 0 40px}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0 40px}}@media screen and (max-width:1000px){.dac-jetpack-page .dac-features .devsite-landing-row-group,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-features.devsite-landing-row-3-up .devsite-landing-row-item,.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item{margin:32px 0 0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos) .devsite-landing-row-group .devsite-landing-row-item:first-of-type{margin-top:0}.dac-jetpack-page .devsite-landing-row-2-up:not(.dac-row-logos){padding-bottom:32px}}@media screen and (max-width:600px){.dac-jetpack-page .dac-row-logos .devsite-landing-row-group{display:block}.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(2),.dac-jetpack-page .dac-components.devsite-landing-row-4-up .devsite-landing-row-item:nth-of-type(4){margin:40px 0 0}.dac-jetpack-page .dac-component h3 p{height:100px}}.dac-kotlin-page .dac-sub-hero-image img{max-height:300px}.dac-kotlin-page .dac-code-snippet .devsite-landing-row-header{margin:0 auto}.dac-kotlin-page .dac-center .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-kotlin-page .dac-components .devsite-landing-row-item-image img{height:80px;width:auto}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;flex-flow:row wrap;margin:0;text-align:center}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-description-content{margin-bottom:64px}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item-content{display:block}.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 8.33333%;flex:1 0 8.33333%;margin:8px 0;padding:8px}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-group{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.dac-kotlin-page .dac-landing-row-item-icon-container-bg-grey .devsite-landing-row-item-icon-container{background-color:#f7f9fa}@media screen and (max-width:1000px){.dac-kotlin-page .dac-features .devsite-landing-row-group{display:block}.dac-kotlin-page .devsite-landing-row-3-up.dac-features .devsite-landing-row-item{margin:32px 0 0}}@media screen and (max-width:840px){.dac-kotlin-page .devsite-landing-row-2-up .dac-landing-row,.dac-kotlin-page .devsite-landing-row-2-up .devsite-landing-row-item-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .devsite-landing-row.dac-label .devsite-landing-row-group{display:block}}@media screen and (max-width:720px){.dac-kotlin-page .dac-app-list .devsite-landing-row-item-list-item{-webkit-box-flex:1;-webkit-flex:1 0 16.66667%;flex:1 0 16.66667%}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-kotlin-page .dac-landing-row-bg-blob-8 .devsite-landing-row-item-buttons{margin-top:0}.dac-kotlin-page .dac-media{-webkit-align-self:center;align-self:center}}.dac-news-page .dac-resource-widget-more,.dac-news-page [maxResults] .dynamic-content-paging{display:none!important}.dac-news-page h1{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-news-page .dac-sign-up .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:70px;text-align:center}.dac-news-page .dac-sign-up .devsite-landing-row-item-description-content{margin:0 0 24px}.dac-news-page .devsite-card-image-bg{padding:0;height:170px}.dac-news-page .devsite-card-wrapper{background:#f7f9fa;-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(25% - 32px);flex:0 0 calc(25% - 32px);overflow:hidden}.dac-news-page .devsite-card,.dac-news-page .devsite-card-content,.dac-news-page .devsite-card>a{-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}.dac-news-page .devsite-card>a{background-color:#f7f9fa}.dac-news-page .devsite-card{height:-webkit-calc(100% + 170px);height:calc(100% + 170px);margin-bottom:-170px;-webkit-transform:translateY(-170px);-o-transform:translateY(-170px);transform:translateY(-170px)}.dac-news-page .devsite-card:hover .devsite-card-content,.dac-news-page .devsite-card:hover>a{-webkit-transform:translateY(170px);-o-transform:translateY(170px);transform:translateY(170px)}.dac-news-page .devsite-card-author{background:#f7f9fa}.dac-news-page .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(rgba(247,249,250,0)),to(#f7f9fa));background:-webkit-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:-o-linear-gradient(rgba(247,249,250,0),#f7f9fa);background:linear-gradient(rgba(247,249,250,0),#f7f9fa);bottom:100%;content:"";height:25px;left:0;position:absolute;width:100%}.dac-news-page [dynamic-card-type=medium] .devsite-card-author{background:rgba(0,0,0,.94)}.dac-news-page [dynamic-card-type=medium] .devsite-card-author:after{background:-webkit-gradient(linear,left top,left bottom,from(transparent),to(rgba(0,0,0,.94)));background:-webkit-linear-gradient(transparent,rgba(0,0,0,.94));background:-o-linear-gradient(transparent,rgba(0,0,0,.94));background:linear-gradient(transparent,rgba(0,0,0,.94))}@media screen and (max-width:1000px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-news-page .devsite-landing-row-group{display:block}.dac-news-page .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin:0}.dac-news-page .dac-sign-up .devsite-landing-row-item-body{padding:35px}}@media screen and (max-width:720px){.dac-news-page .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}}.dac-platforms-sublandings .dynamic-content-paging{display:none}.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 32px);flex:0 0 calc(50% - 32px)}.dac-platforms-sublandings .dac-platforms-sublandings-basics .dac-platforms-img{display:block;margin:16px auto 0;width:480px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group .devsite-landing-row-column{width:100%}.dac-platforms-sublandings .dac-banner-card.devsite-landing-row .dac-button{margin-top:100px}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item{text-align:center}.dac-platforms-sublandings .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image a{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-platforms-sublandings .dac-featured-cards .devsite-landing-row-column img{max-width:75px}.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 32px/40px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1000px){.dac-platforms-sublandings .dac-grow-1 h3,.dac-platforms-sublandings .dac-grow-5 h3{font:300 20px/28px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-platforms-sublandings .dac-grow-1,.dac-platforms-sublandings .dac-sublandings-hero .devsite-landing-row-item-description,.dac-things-page .dac-full-height,.dac-tv-page .dac-platforms-sublandings-basics .dac-full-height{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:40px;margin-top:0}.dac-platforms-sublandings-basics .devsite-landing-row-column .devsite-landing-row-item:last-child{margin-left:0}.dac-platforms-sublandings-basics .devsite-landing-row-column:not(:first-child) .devsite-landing-row-item:last-child{margin:40px 0 0}.dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description{font-weight:300}.dac-oreo-page .dac-list-wrapped a,.dac-platforms-sublandings .dac-sublanding-title.devsite-landing-row:not([background]):not([foreground]) .devsite-landing-row-description,.dac-platforms-sublandings .dac-sublanding-title h2,.dac-preview-page .dac-list-wrapped a,.dac-security-page .dac-list-wrapped a{color:inherit}.dac-platforms-sublandings .dac-sublanding-title .devsite-landing-row-item-description{padding-left:0;padding-top:0}.dac-chrome-page .dac-featured-cards .devsite-landing-row-item-description,.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-platforms-sublandings .devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child).dac-grow-lg-5.dac-full-width-media,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-description,.dac-things-page .devsite-landing-row-item-description:first-of-type{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:0}.dac-auto-page .dac-landing-row-bg-slate,.dac-platforms-sublandings .dac-landing-row-bg-slate.dac-item-switch{margin-top:0}.dac-oreo-page .devsite-landing-row-1-up .devsite-landing-row-item:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-preview-page .devsite-landing-row-1-up .devsite-landing-row-item-description{padding-left:0}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list-item,.dac-security-page .dac-list-wrapped .devsite-landing-row-item-list-item{-webkit-flex-basis:50%;flex-basis:50%;margin:0 0 24px}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-oreo-page .dac-list-wrapped .devsite-landing-row-item-list,.dac-preview-page .dac-list-wrapped .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-landing-row-bg-blob-7 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-auto-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/auto-design.svg)}.dac-platforms-sublandings.dac-auto-page .devsite-landing-row-logos .devsite-landing-row-item .devsite-landing-row-item-image img{height:100%;max-width:115px;width:100%}.dac-auto-page .devsite-landing-row-logos img{width:-webkit-max-content;width:-moz-max-content;width:max-content}.dac-chrome-page .dac-banner-card-bg-img-item{background-image:url(../images/custom/chrome-design.svg)}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{display:block}.dac-tv-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/tv-design.svg)}.dac-wear-page .dac-banner-card .dac-banner-card-bg-img-item{background-image:url(../images/custom/wear-os-design.svg)}.dac-things-page .dac-banner-card.devsite-landing-row img{display:none;margin-top:32px}.dac-things-page .dac-banner-card-bg-img-item.devsite-landing-row-item-no-media{margin:0}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:last-child){margin-left:32px;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.dac-platforms-sublandings .dac-item-switch.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:not(:first-child){margin-left:0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.dac-platforms-sublandings .devsite-landing-row-item-list h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}@media screen and (max-width:1200px){.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-column,.dac-platforms-sublandings-basics.devsite-landing-row .devsite-landing-row-group .devsite-landing-row-item:not(:first-child){margin-left:0;margin-top:40px}.dac-platforms-sublandings .dac-platforms-sublandings-basics .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-chrome-page .dac-featured-cards .devsite-landing-row-group{max-width:100%}.dac-things-page .dac-banner-card.devsite-landing-row img,.dac-tv-page .dac-banner-card.devsite-landing-row img{display:block}.dac-things-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item,.dac-tv-page .dac-banner-card.devsite-landing-row .devsite-landing-row-item{-webkit-box-flex:0;-webkit-flex:none;flex:none}}@media screen and (max-width:720px){.dac-platforms-sublandings .devsite-card-wrapper{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(100% - 32px);flex:0 0 calc(100% - 32px)}.dac-oreo-page .dac-list-wrapped h4,.dac-preview-page .dac-list-wrapped h4,.dac-security-page .dac-list-wrapped h4{font:500 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description,.dac-indie-games .dac-landing-row-bg-slate:not(.dac-stack-items-block-sm) .devsite-landing-row-item-description,.dac-indie-games .devsite-landing-row-item[background=grey] .devsite-landing-row-item-description,.dac-play-guides .dac-landing-row-bg-slate .devsite-landing-row-item-description,.dac-play-guides .dac-stack-items-block-sm .devsite-landing-row-item-description:first-of-type,.dac-subscriptions .dac-card-image--bottom .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin-right:0}.dac-play-guides h3{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;margin-bottom:12px}.dac-play-guides h3 img{-webkit-align-self:baseline;align-self:baseline;margin-bottom:24px}.dac-play-guides .devsite-steps{padding:24px 40px 40px}.dac-play-guides .dac-card-image--bottom .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-guides .dac-illustration-block{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end}.dac-play-guides .dac-illustration-block img{max-width:100%}.dac-android-go-page .devsite-landing-row-3-up h2,.dac-subscriptions .devsite-landing-row-1-up:not(.dac-heading-large) h2{margin-top:40px}.dac-android-go-page .devsite-landing-row[background=grey],.dac-play-guides .devsite-landing-row-1-up.dac-landing-row-bg-slate{background-clip:content-box}.dac-best-practices h3 img{max-height:90px}.dac-best-practices h3 button{display:none}.dac-play-academy .dac-landing-hero{background:url(../images/custom/play-academy-banner.svg) bottom/35% no-repeat;margin-bottom:0;padding-bottom:180px}.dac-play-academy .dac-cards-row .devsite-landing-row-group{background-color:#fff;margin:0 32px;padding:32px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{-webkit-flex-basis:58%;flex-basis:58%}.dac-play-academy .dac-cards-row img{min-width:120px}.dac-play-academy .dac-play-academy-footer{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-academy .dac-play-academy-footer p{text-align:center}.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background:url(../images/custom/indie-games-indie-corner-background.svg) 100% 100%/auto 70% no-repeat;margin-right:0}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse}.dac-indie-games .dac-landing-row-item-device-image{-webkit-box-flex:0;-webkit-flex:0 0 30%;flex:0 0 30%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:100%}.dac-startups-page .dac-sublanding-title-normal{margin-top:64px}.dac-startups-page .dac-illustration-block{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin-top:64px}.dac-android-go-page .devsite-landing-row[background=grey] .devsite-landing-row-item-description{padding:32px}@media screen and (max-width:1400px){.dac-play-guides:not(.dac-play-academy):not(.dac-best-practices) .devsite-landing-row-3-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-guides .devsite-landing-row-3-up .devsite-landing-row-item.dac-landing-row-bg-slate{margin:32px 0 0}.dac-indie-games .devsite-landing-row-3-up:last-of-type .devsite-landing-row-item:first-of-type,.dac-startups-page .devsite-landing-row-3-up:not(.force-spacing) .devsite-landing-row-group div:first-of-type{margin-top:0}.dac-subscriptions .devsite-landing-row-3-up{padding-bottom:0}}@media screen and (max-width:1200px){.dac-indie-games .dac-landing-row-item-indie-corner .devsite-landing-row-item-description{background-image:none}.dac-indie-games .dac-landing-row-item-device .devsite-landing-row-item-description-content{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-2-up .devsite-landing-row-group,.dac-indie-games .devsite-landing-row-3-up .devsite-landing-row-group,.dac-startups-page .devsite-landing-row-2-up .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-families-page .devsite-landing-row-2-up .devsite-landing-row-item.dac-card-image--bottom,.dac-play-guides .devsite-landing-row-2-up .devsite-landing-row-item.dac-stack-items-block-sm{margin:32px 0 0}.dac-play-academy .dac-cards-row img{width:85px}.dac-play-academy .dac-cards-row .devsite-landing-row-item:last-of-type{margin:0}.dac-play-academy .dac-cards-row .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-academy .dac-landing-hero{-o-background-size:50%;background-size:50%}.dac-indie-games .dac-landing-row-item-device-image img{max-width:30%}}.dac-gpi-page h2{color:#414141}.dac-gpi-page .dac-landing-row-hero img{margin-bottom:40px;max-width:380px}.dac-gpi-page .video-wrapper{float:none;margin:auto auto 40px;width:-webkit-calc(100% - 40px);width:calc(100% - 40px)}.dac-gpi-page .dac-landing-row-bg-slate{background-clip:content-box}.dac-gpi-page .dac-center .devsite-landing-row-item-description,.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-gpi-page .dac-landing-row-bg-slate .devsite-landing-row-item-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:55px 0 7px}.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding-left:32px}@media screen and (max-width:1200px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}@media screen and (max-width:1000px){.dac-gpi-page .dac-landing-row-bg-slate h2{font:300 32px/38px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}}.dac-gpi-page .dac-landing-row-bg-slate h4{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-gpi-page .dac-landing-row-bg-slate li{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0 0 24px;padding-left:32px}.dac-play-page .dynamic-content-paging{display:none}.dac-play-sublandings .dac-sublandings-hero{padding:86px 0}.dac-play-sublandings .dac-banner-img .devsite-landing-row-item-no-description{height:288px;padding-bottom:0;padding-top:0}.dac-play-sublandings .dac-sublandings-hero-copy img{margin-bottom:16px;width:54px}.dac-play-sublandings .dac-sublandings-hero h1{color:inherit;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-sublandings-section-header .dac-sublandings-section-header-img img{display:block;margin:0 auto;max-width:500px}.dac-play-billing-page .dac-sublandings-section-header img,.dac-play-store-page .dac-sublandings-section-header .dac-sublandings-section-header-img .devsite-landing-row-item-description-content{min-height:230px}.dac-play-sublandings .dac-success-story{margin-left:40px}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:0;-webkit-flex:0 0 -webkit-calc(50% - 19px);flex:0 0 calc(50% - 19px)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item,.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item,.dac-play-console-page .dac-cta .devsite-landing-row-item-description,.dac-play-store-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:nth-of-type(3)),.dac-play-sublandings .dac-resources-heading .devsite-landing-row-item-description{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{max-width:580px}.dac-play-page .dac-illustration-play-console h3{margin-bottom:26px}.dac-play-page .dac-grow-5 .devsite-landing-row-item-description{display:-webkit-box;display:-webkit-flex;display:flex}.dac-play-page .dac-landing-row-bg-illustration-3:after{min-height:300px}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:32px;left:32px;position:absolute}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:68.5%;flex-basis:68.5%;margin-bottom:0}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{background:url(../images/custom/google-play-console-grow-business.svg) 0 100%/700px no-repeat}.dac-play-console-page .dac-sublandings-section-header .devsite-landing-row-item-description{padding:32px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-group{background:url(../images/custom/google-play-console-release-confidence.svg) 100% 100%/700px no-repeat}.dac-play-console-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-console-focus-quality.svg) 0/cover no-repeat}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-align-self:center;align-self:center;-webkit-box-flex:1;-webkit-flex:auto;flex:auto;margin-left:32px;width:-webkit-calc(73% - 16px);width:calc(73% - 16px)}.dac-play-console-page .dac-bottom-spacing:not(.dac-bg-img-in-column) .dac-heading-medium{margin:32px 0 0 20px}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:first-of-type{margin:0}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type{margin:0 0 0 32px}.dac-play-console-page .dac-landing-row-bg-mint:hover{background-color:#55ffb5;color:#414141}.dac-play-console-page .dac-landing-row-bg-yellow:hover{background-color:#ffd600}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.dac-play-console-page .dac-success-story .devsite-landing-row-item-image img{display:inline-block;float:right;width:120px}.dac-play-console-page .dac-bottom-spacing.dac-bg-img-in-column .devsite-landing-row-item{margin-right:0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{max-width:33%}.dac-play-console-page .dac-cta .devsite-landing-row-item-buttons{margin:auto}.dac-play-console-page .dac-chevron-right{color:#fff}.dac-play-store-page .dac-sublandings-hero{padding-bottom:0}.dac-play-store-page .dac-banner-img .devsite-landing-row-item-no-description{background:url(../images/custom/google-play-store-get-discovered.svg) 0/cover no-repeat}.dac-play-store-page .dac-sublandings-hero-image img{display:block;margin:auto}.dac-play-store-page .dac-resources-heading .devsite-landing-row-item{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-play-store-page .dac-cards-2-1-1 .devsite-landing-row-item-description-content a,.dac-play-store-page .dac-cards-2-1-1 [background]:not([background=grey]) p>a:not(.button){color:#1a73e8;text-decoration:none}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:32px 0 0 20px}.dac-play-services-page .dac-sublandings-hero-copy .devsite-landing-row-item-buttons,.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium{margin-top:32px}.dac-play-services-page .dac-sublandings-section-header img{min-height:240px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-description{padding:0}.dac-resources-page h3:first-child{color:#1a73e8;font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:32px}.dac-resources-page .devsite-landing-row-3-up .devsite-landing-row-item-media{margin-bottom:0}.dac-play-policies-page .dac-code-snippet img{margin:0 auto}@media screen and (max-width:1400px){.dac-play-sublandings .dac-success-story{margin-left:0}.dac-play-sublandings .dac-sublandings-section-header .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-console-page .devsite-landing-row-4-up .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}}@media screen and (max-width:1200px){.dac-play-sublandings .dac-sublandings-hero h1{font:300 38px/44px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.dac-play-billing-page .dac-cards-2-1-1 .devsite-landing-row-item,.dac-play-services-page .devsite-landing-row-4-up.dac-bottom-spacing .devsite-landing-row-item,.dac-play-sublandings .dac-cards-2-1-1.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child),.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2.devsite-landing-row:not(.devsite-landing-row-4-up) .devsite-landing-row-item-no-media:not(:first-child){margin:32px 0 0}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-group,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 1 40%;flex:1 1 40%}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-item:last-of-type,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:nth-of-type(3){margin-left:0;margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-page .devsite-landing-row-4-up .devsite-landing-row-group .dac-grow-4:last-of-type{margin-top:32px}.dac-play-console-page .dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1 0 -webkit-calc((100% - 40px)/2);flex:1 0 calc((100% - 40px)/2)}.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column,.dac-play-services-page .devsite-landing-row-4-up .devsite-landing-row-group,.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-group,.dac-play-sublandings:not(.dac-play-console-page) .dac-cards-1-1-2 .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}}@media screen and (max-width:1000px){.dac-play-sublandings .dac-cards-1-1-2 .devsite-landing-row-item:nth-of-type(3),.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{-webkit-flex-basis:auto;flex-basis:auto}.dac-play-sublandings .dac-sublandings-hero-copy,.dac-play-sublandings .dac-sublandings-hero .devsite-landing-row-group{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-play-billing-page .dac-sublandings-hero-image img,.dac-play-services-page .dac-sublandings-hero-image img{min-height:400px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:first-of-type,.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type,.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(2),.dac-play-console-page .devsite-landing-row-4-up:not(.devsite-landing-row-logos) .devsite-landing-row-item:nth-of-type(4){margin-top:32px}.dac-play-page .dac-grow-5,.dac-play-page .dac-grow-7{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-play-page .devsite-landing-row:not(.devsite-landing-row-4-up) .dac-grow-4{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;margin-left:0;margin-top:32px}.dac-play-page .dac-basis-9.devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%}.dac-play-page .devsite-landing-row.dac-landing-row-bg-grey .devsite-landing-row-group{-webkit-flex-wrap:wrap;flex-wrap:wrap}.dac-play-console-page .dac-sublandings-hero .devsite-landing-row-item-media{-webkit-box-flex:0;-webkit-flex:none;flex:none;margin-left:0;-webkit-box-ordinal-group:4;-webkit-order:3;order:3;padding-top:20px;width:100%}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:last-of-type,.dac-play-console-page .devsite-landing-row-4-up.dac-bottom-spacing .dac-heading-medium,.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item{-webkit-flex-basis:100%;flex-basis:100%;margin-left:0}.dac-play-store-page .dac-cards-1-1-1-1 .devsite-landing-row-group .devsite-landing-row-item:first-child{margin-top:0}.dac-play-store-page .devsite-landing-row.dac-cards-1-1-1-1+.dac-cards-1-1-1-1{padding-top:20px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item{-webkit-box-flex:1;-webkit-flex:1;flex:1;max-width:100%}.dac-play-billing-page.dac-play-sublandings .dac-cards-2-1-1 .devsite-landing-row-item:first-of-type{margin-bottom:0}.dac-play-policies-page .dac-sublandings-hero-copy{margin-bottom:32px}}@media screen and (max-width:720px){.dac-play-sublandings .dac-landing-row-item-icon-container-left{margin-top:0}.dac-play-page .dac-landing-row-bg-blob-3 .devsite-landing-row-item-buttons,.dac-play-page .dac-landing-row-bg-illustration-3 .devsite-landing-row-item-buttons{bottom:24px;left:24px}.dac-play-billing-page .devsite-landing-row-2-up.dac-bottom-spacing.devsite-landing-row:not(.devsite-landing-row-logos) .devsite-landing-row-item:last-of-type{margin-top:32px}}@media screen and (max-width:600px){.dac-play-console-page .dac-bg-img-in-row .devsite-landing-row-column:last-of-type{height:300px}.dac-play-console-page .dac-bg-img-in-column .devsite-landing-row-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;height:300px}.dac-play-console-page .dac-cards-1-1-2 .devsite-landing-row-item:not(:first-child){margin-left:0}.dac-play-services-page .devsite-landing-row-3-up.dac-bottom-spacing .devsite-landing-row-item.dac-heading-medium:first-of-type{margin-top:0}}.dac-security-page .dac-slate{color:#455a64}.dac-subscribe-page .dac-subscribe-heading{color:#414141;margin:24px 0 40px}.dac-subscribe-page .dac-field,.dac-subscribe-page .dac-field-group{margin:0 4px 32px}.dac-subscribe-page .dac-names-field{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;width:60%}.dac-subscribe-page .dac-names-field .dac-field{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:260px}.dac-subscribe-page label,.dac-subscribe-page legend{color:#202124;font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400}.dac-subscribe-page .dac-label-description{color:#5f6368;font:500 12px/18px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:400;line-height:24px;margin:0 32px}.dac-subscribe-page .dac-language-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center}[dir=ltr] .dac-subscribe-page .dac-language-container label{margin-right:12px}.dac-subscribe-page .dac-form-info{font:400 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-subscribe-page .dac-form-after-submit h1{color:#414141;font:300 56px/64px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin-top:0}.dac-subscribe-page .dac-form input[type=text]{padding:10px}.dac-subscribe-page .dac-form input[type=email]:focus{border-bottom:2px solid #4285f4}.dac-subscribe-page .dac-form .dac-email{margin-bottom:0;min-width:260px;width:60%}.dac-subscribe-page devsite-framebox{margin-left:20px}.dac-subscribe-page.dac-subscribe-container{background-color:#f7f9fa;margin:0 auto;max-width:1000px;padding:50px;position:relative}.dac-subscribe-page iframe{width:100%}.dac-subscribe-page .dac-form .dac-email{-webkit-border-radius:2px;border-radius:2px;margin-bottom:40px;min-width:225px;padding:10px;width:70%}.dac-subscribe-page .dac-language-container{display:-webkit-box;display:-webkit-flex;display:flex;margin:45px 0 30px}.dac-subscribe-page .dac-language-container h3,.dac-subscribe-page .dac-row{margin:0}.dac-subscribe-page .dac-form-icon{height:230px;position:absolute;right:80px;top:495px}@media screen and (max-width:1024px){.dac-subscribe-page .dac-form-icon .dac-subscribe-page .dac-form-icon{display:none}}.dac-subscribe-page .dac-outline-button{margin-top:45px}@media screen and (max-width:820px){.dac-subscribe-page .dac-form-icon{display:none}body[layout=full][type=landing].dac-subscribe-page .devsite-main-content{padding:0}.dac-subscribe-page .dac-subscribe-container{padding:25px}.dac-subscribe-page .dac-form .dac-email{width:85%}}@media screen and (max-width:470px){.dac-subscribe-page .dac-language-container{display:block}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}.dac-studio-page .download td{vertical-align:middle}.dac-studio-page #studio-downloaded-dialog{height:auto}.dac-studio-page #studio-downloaded-dialog .devsite-dialog-contents{height:-webkit-calc(100% - 36px);height:calc(100% - 36px);margin-bottom:24px}.dac-studio-page .studio-downloaded-links{padding:16px 0}.dac-studio-page .studio-downloaded-links img{background:#d7d7d7;margin:0 0 16px;width:100%}.dac-studio-page .devsite-landing-row-item-description ul{padding-left:16px}.dac-studio-page .dac-info-size{font-size:smaller;margin-top:4px;padding:0 8px}.dac-studio-page .dac-sizes{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.dac-studio-page .dac-feature-last{margin-bottom:40px}.dac-studio-page .dac-btn-block{margin-top:32px;padding:0 32px}.dac-studio-page .dac-landing-row-hero{margin-bottom:32px}.dac-studio-page .dac-download-options{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:100%}.dac-studio-page .dac-download-info{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;height:100%;padding:24px 12px;text-align:center}.dac-studio-page .dac-download-info h2{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;font-weight:500}.dac-studio-page .dac-download-info.show{-webkit-box-flex:6;-webkit-flex:6;flex:6;margin:0 32px 0 0}.dac-studio-page .dac-download-info.solo{-webkit-box-flex:4;-webkit-flex:4;flex:4;max-width:500px}.dac-studio-page .dac-landing-links{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:24px 0;width:100%}.dac-studio-page .dac-landing-links .button{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:12px 40px;min-width:240px;text-align:center}.dac-studio-page .dac-dropshadow{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}.dac-studio-page .dac-video-card h3{font:500 22px/30px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}.dac-studio-page .devsite-landing-row-item-media{background:#455a64;display:-webkit-box;display:-webkit-flex;display:flex}.dac-studio-page .devsite-landing-row-item-image{-webkit-align-self:center;align-self:center;background:#455a64;width:100%}.dac-terms-page .sdk-terms{white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall{height:450px;overflow:auto}.dac-archive-page .sdk-terms{color:#757575;border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;font:400 14px/22px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:300px;margin-bottom:16px;overflow:scroll;padding:12px;white-space:pre-wrap;word-wrap:break-word}.dac-archive-page.tos-wall h2,.dac-archive-page.tos-wall h3{font:500 16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0}.dac-releases-page.updates-box{border:1px solid #dadce0;-webkit-box-shadow:none;box-shadow:none;margin:12px 0;padding:12px 24px 0}.dac-test-page .jd-sumtable th{color:inherit}@media screen and (max-width:1000px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){margin:0}.dac-full-width-content .devsite-landing-row-group .devsite-landing-row-item-no-media,.dac-full-width-content .devsite-landing-row .devsite-landing-row-item-no-media{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-btn-block{padding:0}.dac-studio-page .dac-landing-links .button{margin:12px 16px}.dac-studio-page .dac-download-options{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.dac-studio-page .dac-download-info.show,.dac-studio-page .dac-download-info.solo{-webkit-box-flex:1;-webkit-flex:1;flex:1}.dac-studio-page .dac-download-info.solo{max-width:100%}.dac-studio-page .dac-download-info.show{margin:0 0 16px}}@media screen and (max-width:720px){.dac-full-width-content .devsite-landing-row-item-no-media+.devsite-landing-row-item-no-media:nth-of-type(2n),.dac-full-width-content .devsite-landing-row-item-no-media:not(:first-child){-webkit-box-flex:0;-webkit-flex:0;flex:0}}devsite-book-nav{max-height:100vh;overflow-x:hidden;overflow-y:auto;position:relative;z-index:1004}body[pending] devsite-book-nav{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);height:100vh}body[ready] devsite-book-nav[fixed]{-webkit-box-shadow:none;box-shadow:none;contain:content;max-height:100%;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:top,max-height,transform}.devsite-book-nav-bg{background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 1px 3px 1px rgba(60,64,67,.15)}body[ready] .devsite-book-nav-bg[fixed]{bottom:0;display:block;position:fixed;top:0;z-index:1}.devsite-book-nav-bg:after{bottom:-10px;content:"";display:block;height:10px;left:0;position:fixed;width:278px}[dir=rtl] .devsite-book-nav-bg:after{left:auto;right:0}devsite-book-nav .devsite-nav{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-book-nav .devsite-nav-list{padding-bottom:36px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{border-top:0;margin-top:20px;padding-top:0}devsite-book-nav li .devsite-nav-title{padding-left:24px}[dir=rtl] devsite-book-nav li .devsite-nav-title{padding-left:8px;padding-right:24px}devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-title{padding-left:8px;padding-right:40px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-title{padding-left:8px;padding-right:56px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-title{padding-left:8px;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-title{padding-left:8px;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li li .devsite-nav-title{padding-left:8px;padding-right:104px}devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-left:0}[dir=rtl] devsite-book-nav li.devsite-nav-divider .devsite-nav-title{padding-right:0}devsite-book-nav .devsite-nav-title{padding-right:8px}[dir=rtl] devsite-book-nav .devsite-nav-title{padding-left:8px;padding-right:0}devsite-book-nav .devsite-nav-list>.devsite-nav-heading:not(.devsite-nav-divider){border-top:1px solid #dadce0;padding-top:11px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider){margin-top:12px}devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:24px}[dir=rtl] devsite-book-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:24px}devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:40px}[dir=rtl] devsite-book-nav devsite-expandable-nav .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:40px}devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:56px}[dir=rtl] devsite-book-nav devsite-expandable-nav li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:56px}devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:72px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:72px}devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:88px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:88px}devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:104px}[dir=rtl] devsite-book-nav devsite-expandable-nav li li li li .devsite-nav-heading:not(.devsite-nav-divider) .devsite-nav-title-no-path{padding-left:0;padding-right:104px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider{background:#eceff1;border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:4px 24px 2px}devsite-book-nav .devsite-nav-heading.devsite-nav-divider:first-child{padding-top:4px}devsite-book-nav .devsite-nav-divider>.devsite-nav-title{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;text-transform:uppercase}devsite-book-nav .devsite-nav-accordion+.devsite-nav-accordion,devsite-book-nav .devsite-nav-divider+.devsite-nav-accordion{border-top:0;padding-top:12px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-divider{border-top:0;padding-top:4px}devsite-book-nav .devsite-nav-accordion+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-divider+.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider),devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-accordion,devsite-book-nav .devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider)+.devsite-nav-divider{margin-top:12px}devsite-book-nav .devsite-nav-break{height:24px}#devsite-hamburger-menu,#devsite-hamburger-menu[visually-hidden],devsite-book-nav .devsite-mobile-header,devsite-book-nav .devsite-mobile-nav-top{display:none}#devsite-hamburger-menu:before{content:"menu"}devsite-book-nav #devsite-close-nav:before{content:"arrow_back"}[dir=rtl] devsite-book-nav #devsite-close-nav:before{content:"arrow_forward"}devsite-book-nav[top-level-nav] #devsite-close-nav:before{content:"close"}@media screen and (max-width:840px){devsite-book-nav{display:none;height:100vh;max-height:100vh!important;top:0!important;-webkit-transform:translate3d(-280px,0,0)!important;transform:translate3d(-280px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1);z-index:1013}[dir=rtl] devsite-book-nav{-webkit-transform:translate3d(280px,0,0)!important;transform:translate3d(280px,0,0)!important}devsite-book-nav:not([animatable]){-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}body[ready] .devsite-book-nav-bg[fixed]{display:none}body[ready] devsite-book-nav[fixed]{background:#fff;display:block!important;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-book-nav[visually-hidden]{opacity:1!important;pointer-events:auto!important;visibility:visible!important}#devsite-hamburger-menu{display:inline-block}devsite-book-nav #devsite-close-nav{color:#5f6368;-webkit-flex-shrink:0;flex-shrink:0}#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{height:auto;padding:8px;position:relative;width:auto;z-index:20}[dir=ltr] #devsite-hamburger-menu,[dir=ltr] devsite-book-nav #devsite-close-nav{margin:0 8px 0 -4px}[dir=rtl] #devsite-hamburger-menu,[dir=rtl] devsite-book-nav #devsite-close-nav{margin:0 -4px 0 8px}devsite-book-nav .devsite-mobile-nav-top{display:block}devsite-book-nav .devsite-book-nav-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transform:translate3d(-268px,0,0)!important;transform:translate3d(-268px,0,0)!important;-webkit-transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);transition:-webkit-transform .2s cubic-bezier(.4,0,.2,1);-o-transition:-o-transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1);transition:transform .2s cubic-bezier(.4,0,.2,1),-webkit-transform .2s cubic-bezier(.4,0,.2,1),-o-transform .2s cubic-bezier(.4,0,.2,1)}[dir=rtl] devsite-book-nav .devsite-book-nav-wrapper{-webkit-transform:translate3d(268px,0,0)!important;transform:translate3d(268px,0,0)!important}devsite-book-nav:not([animatable]) .devsite-book-nav-wrapper{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-book-nav .devsite-nav-list{padding-bottom:120px}devsite-book-nav .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:0}devsite-book-nav .devsite-mobile-nav-bottom .devsite-nav-list>.devsite-nav-item:not(.devsite-nav-accordion):not(.devsite-nav-divider):first-child{margin-top:13px}devsite-book-nav .devsite-mobile-nav-top .devsite-nav-text{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-book-nav[top-level-nav] .devsite-book-nav-wrapper{-webkit-transform:translateZ(0)!important;transform:translateZ(0)!important}devsite-book-nav:not([top-level-nav]) .devsite-mobile-nav-top,devsite-book-nav[top-level-nav] .devsite-mobile-nav-bottom{height:-webkit-calc(100vh - 64px);height:calc(100vh - 64px);overflow:hidden}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item{border-bottom:1px solid #dadce0}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title{font-weight:700;padding-bottom:15px;padding-top:16px}devsite-book-nav .devsite-mobile-nav-top>.devsite-nav-list>.devsite-nav-item>.devsite-nav-title:not(.devsite-nav-active){color:#5f6368}devsite-book-nav .devsite-mobile-nav-bottom,devsite-book-nav .devsite-mobile-nav-top{-webkit-flex-shrink:0;flex-shrink:0;width:268px}devsite-book-nav .devsite-mobile-header{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 16px;position:relative}devsite-book-nav .devsite-mobile-header .devsite-nav-active{font-weight:400}devsite-book-nav .devsite-nav-responsive-tabs{margin-bottom:12px;margin-top:-11px}devsite-book-nav .devsite-lower-tab-item{margin:0}devsite-book-nav .devsite-nav-responsive-tabs>.devsite-nav-item:last-child{margin-bottom:8px}}@media screen and (max-width:600px){#devsite-hamburger-menu,devsite-book-nav #devsite-close-nav{margin:0 4px 0 -12px}}devsite-book-nav .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-book-nav .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-book-nav .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-book-nav .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-book-nav .devsite-product-description{margin:0 0 0 180px}devsite-book-nav .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-book-nav .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-book-nav .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-book-nav .devsite-product-description,devsite-book-nav .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-book-nav .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-book-nav .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-book-nav .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-book-nav .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-book-nav .devsite-product-name-link,devsite-book-nav .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-book-nav .devsite-product-name-link:focus,devsite-book-nav .devsite-product-name-link:hover,devsite-book-nav .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-book-nav .devsite-site-logo{height:32px}devsite-book-nav .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-book-nav .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-book-nav .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-book-nav .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-book-nav .devsite-site-logo-link canvas{height:auto!important}devsite-book-nav .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-book-nav .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-book-nav .devsite-product-logo-container{margin-left:4px}devsite-book-nav .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-book-nav .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-book-nav .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-book-nav .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-book-nav .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-book-nav .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-book-nav .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-book-nav .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-book-nav .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-expandable-nav{cursor:pointer;display:block;position:relative}devsite-expandable-nav>.devsite-nav-section{max-width:100%;overflow-y:hidden;-webkit-transition:height .2s;-o-transition:height .2s;transition:height .2s;width:100%;will-change:height}devsite-expandable-nav:not([animatable])>.devsite-nav-section{-webkit-transition:height 1ms;-o-transition:height 1ms;transition:height 1ms}devsite-expandable-nav[collapsed]:not([animating])>.devsite-nav-section{display:none}devsite-expandable-nav[collapsed]:not([connected])>.devsite-nav-section{height:0}devsite-expandable-nav>.devsite-nav-title-no-path{cursor:pointer;outline:0}devsite-expandable-nav>.devsite-nav-title{padding-left:24px}[dir=rtl] devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:24px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:40px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:40px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:56px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:56px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:72px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:72px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:88px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:88px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:104px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-title{padding-left:0;padding-right:104px}devsite-expandable-nav>.devsite-nav-toggle{color:#bdc1c6;cursor:pointer;font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav>.devsite-nav-toggle{font-size:18px;position:absolute;top:2px;-webkit-transform:rotate(0);-o-transform:rotate(0);transform:rotate(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease;will-change:transform}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}devsite-expandable-nav>.devsite-nav-toggle{left:4px}[dir=rtl] devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:4px}devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:20px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:20px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:36px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:36px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:52px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:52px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:68px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:68px}devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:84px}[dir=rtl] devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav devsite-expandable-nav>.devsite-nav-toggle{left:auto;right:84px}.devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}[dir=rtl] .devsite-nav-item:not(.devsite-nav-accordion)>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}devsite-expandable-nav>.devsite-nav-toggle:before{content:"arrow_drop_down"}.devsite-nav-accordion{border-bottom:1px solid #dadce0;border-top:1px solid #dadce0;padding:11px 0}.devsite-nav-accordion>devsite-expandable-nav{-webkit-flex-wrap:wrap;flex-wrap:wrap}.devsite-nav-accordion>devsite-expandable-nav,.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-title{color:rgba(0,0,0,.65);-webkit-box-flex:1;-webkit-flex:1 0 196px;flex:1 0 196px;font-weight:700;overflow:hidden}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{font-size:24px;margin:0 8px 0 0;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;-webkit-transform:rotateX(0deg);transform:rotateX(0deg);-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}[dir=rtl] .devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle{margin:0 0 0 8px}.devsite-nav-accordion>devsite-expandable-nav:not([animatable])>.devsite-nav-toggle{-webkit-transition:-webkit-transform 1ms;transition:-webkit-transform 1ms;-o-transition:-o-transform 1ms;transition:transform 1ms;transition:transform 1ms,-webkit-transform 1ms,-o-transform 1ms}.devsite-nav-accordion>devsite-expandable-nav[collapsed]>.devsite-nav-toggle{-webkit-transform:rotateX(180deg);transform:rotateX(180deg)}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-toggle:before{content:"expand_less"}.devsite-nav-accordion>devsite-expandable-nav>.devsite-nav-section{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}devsite-footer-linkboxes{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex:1;flex:1;margin:24px 0}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:24px}devsite-footer-linkboxes .devsite-footer-linkbox-heading{font:500 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 0 8px}devsite-footer-linkboxes .devsite-footer-linkbox-list .devsite-footer-linkbox-heading{margin-top:40px}devsite-footer-linkboxes .devsite-footer-linkbox-list{list-style-type:none;padding:0}devsite-footer-linkboxes .devsite-footer-linkbox-item{margin:0}devsite-footer-linkboxes .devsite-footer-linkbox-link{color:#202124;display:block;padding:8px 0}devsite-footer-linkboxes .devsite-footer-linkbox-link:focus,devsite-footer-linkboxes .devsite-footer-linkbox-link:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}}@media screen and (max-width:600px){devsite-footer-linkboxes{padding:0 16px}devsite-footer-linkboxes .devsite-footer-linkboxes-list{display:block}devsite-footer-linkboxes .devsite-footer-linkbox{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-footer-linkboxes .devsite-footer-linkbox:not(:first-child){margin-left:0}}devsite-footer-promos{border-top:1px solid #dadce0;background:#fff;display:block;font:14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-promos .devsite-footer-promos-list{border-bottom:1px solid #dadce0;display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;-webkit-justify-content:space-around;justify-content:space-around;padding:18px 0}devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 1 192px;flex:0 1 192px;margin:20px 0;text-align:center}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:24px}devsite-footer-promos .devsite-footer-promo-icon{color:rgba(0,0,0,.87);display:block;font-size:48px;height:48px;margin:0 auto 8px;width:48px}devsite-footer-promos .devsite-footer-promo-title{color:rgba(0,0,0,.87);display:block;font-weight:500}devsite-footer-promos .devsite-footer-promo-title:focus,devsite-footer-promos .devsite-footer-promo-title:hover{color:#1a73e8;text-decoration:none}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;padding:0 20px}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}}@media screen and (max-width:840px){.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promos-list,devsite-footer-promos .devsite-footer-promos-list{-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:12px 0}.devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,devsite-footer-promos .devsite-footer-promo{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%;margin:0;padding:8px 8px 8px 0;text-align:left}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-promos .devsite-footer-promo,[dir=rtl] devsite-footer-promos .devsite-footer-promo{text-align:right}devsite-footer-promos .devsite-footer-promo:not(:first-child){margin-left:0}devsite-footer-promos .devsite-footer-promo-icon{height:32px;margin:0 8px 0 0;width:32px}devsite-footer-promos .devsite-footer-promo-title{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font-weight:400}devsite-footer-promos .devsite-footer-promo-description{display:none}}@media screen and (max-width:600px){devsite-footer-promos{padding:0 16px}devsite-footer-promos .devsite-footer-promos-list{display:block}}devsite-footer-utility{background:#fff;display:block;font:400 14px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:0 24px}devsite-footer-utility nav{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;padding:24px 0}devsite-footer-utility .devsite-footer-sites{-webkit-box-align:center;-webkit-align-items:center;align-items:center;border-bottom:1px solid #dadce0;padding:24px 0 23px}devsite-footer-utility .devsite-footer-sites-list{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;padding:0}devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}devsite-footer-utility .devsite-footer-sites-link{color:#202124;display:block;padding:8px 0}devsite-footer-utility .devsite-footer-sites-link:focus,devsite-footer-utility .devsite-footer-sites-link:hover{color:#1a73e8;text-decoration:none}devsite-footer-utility .devsite-footer-sites-logo-link{display:-webkit-box;display:-webkit-flex;display:flex}devsite-footer-utility .devsite-footer-sites-logo{height:32px;margin-top:-4px;width:185px}devsite-footer-utility .devsite-footer-utility-list{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;list-style:none;min-height:36px;padding:0}devsite-footer-utility .devsite-footer-utility-link{color:#202124}devsite-footer-utility .devsite-footer-utility-link:focus,devsite-footer-utility .devsite-footer-utility-link:hover{color:#1a73e8}devsite-footer-utility .devsite-footer-utility-item{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item:last-child{margin-right:0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:last-child{margin-left:0}devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{content:"|";margin:0 8px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-utility-item:not(:first-child):before{margin:0 0 0 8px}devsite-footer-utility .devsite-footer-utility-item.devsite-footer-utility-button:before{content:"";margin:0}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;line-height:20px;margin-left:auto;padding-left:16px}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-flex-shrink:0;flex-shrink:0;margin:0 0 0 16px}[dir=rtl] devsite-footer-utility .devsite-footer-utility-button>a{margin:0 16px 0 0}devsite-footer-utility .devsite-footer-utility-button>a:focus{text-decoration:none}devsite-footer-utility devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-footer-utility devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:1252px){.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] .devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px}}@media screen and (max-width:840px){devsite-footer-utility .devsite-footer-sites{display:block}devsite-footer-utility .devsite-footer-sites-item{margin:0 40px 0 0}[dir=rtl] devsite-footer-utility .devsite-footer-sites-item{margin:0 0 0 40px}devsite-footer-utility .devsite-footer-sites-logo{margin-bottom:16px;margin-top:0}}@media screen and (max-width:600px){devsite-footer-utility{padding:0 16px}devsite-footer-utility .devsite-footer-sites,devsite-footer-utility .devsite-footer-sites-list,devsite-footer-utility nav{display:block}.devsite-main-content[has-book-nav]~devsite-footer-utility .devsite-footer-sites-item,devsite-footer-utility .devsite-footer-sites-item{margin:0}devsite-footer-utility devsite-language-selector{display:block;margin:16px 0 0}devsite-footer-utility .devsite-footer-utility-list{-webkit-flex-wrap:wrap;flex-wrap:wrap}devsite-footer-utility .devsite-footer-utility-button{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;margin:16px 0 0;padding:0}devsite-footer-utility .devsite-footer-utility-button>a{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}}devsite-header{display:block;position:relative;z-index:1006}body[ready] devsite-header[fixed]{contain:layout;pointer-events:none;position:fixed;top:0;width:100%}devsite-header .devsite-top-logo-row-wrapper-wrapper{position:relative;z-index:1}body[ready] devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper:before{content:"";height:400px;position:absolute;-webkit-transform:translateY(-400px);-o-transform:translateY(-400px);transform:translateY(-400px);width:100%}devsite-header[fixed] .devsite-top-logo-row-wrapper-wrapper{pointer-events:all}devsite-header .devsite-collapsible-section{position:relative}devsite-header .devsite-collapsible-section,devsite-header[no-lower-row][fixed]{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-header[fixed] .devsite-collapsible-section{contain:style;pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:transform}devsite-header .devsite-top-logo-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;height:48px;padding:0 24px;position:relative}devsite-header .devsite-top-button{background:0;padding:0 8px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}devsite-header .devsite-top-button,devsite-header .devsite-top-button:active,devsite-header .devsite-top-button:focus,devsite-header .devsite-top-button:hover{border:0}devsite-header .devsite-header-icon-button{display:none;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:24px;min-width:24px;padding:0;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;width:24px}devsite-header .devsite-top-logo-row-middle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}@media screen and (max-width:840px){devsite-header{-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-header .devsite-top-logo-row{padding:0 16px}devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-header-upper-tabs devsite-tabs{margin:0 16px 0 0}devsite-header .devsite-header-upper-tabs .devsite-doc-set-nav{display:none}}devsite-header .devsite-header-billboard{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:40px 24px 20px;position:relative;z-index:100}devsite-header .devsite-header-billboard h1{font:300 24px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;line-height:1;margin:14px 0;overflow:visible;padding:0}devsite-header .devsite-header-billboard-logo{max-height:64px}devsite-header .devsite-header-billboard-search{margin:0 auto;max-width:816px;padding-bottom:48px}devsite-header .devsite-header-billboard-search devsite-search{width:100%}devsite-header .devsite-header-billboard-search devsite-search .devsite-popout-result{max-height:50vh}@media screen and (max-width:840px){devsite-header .devsite-header-billboard-search{margin:0 24px}}devsite-header .devsite-doc-set-nav-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:48px;padding:0 24px 0 0}[dir=rtl] devsite-header .devsite-doc-set-nav-row{padding:0 0 0 24px}[dir=ltr] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-left:24px}[dir=rtl] devsite-header .devsite-doc-set-nav-row .devsite-breadcrumb-list{padding-right:24px}@media screen and (max-width:840px){devsite-header .devsite-doc-set-nav-row{display:none}}devsite-header devsite-language-selector{margin:0 0 0 16px}[dir=rtl] devsite-header devsite-language-selector{margin:0 16px 0 0}@media screen and (max-width:840px){devsite-header devsite-language-selector{margin:0 0 0 8px}[dir=rtl] devsite-header devsite-language-selector{margin:0 8px 0 0}}@media screen and (max-width:600px){devsite-header devsite-language-selector{display:none}}devsite-header .devsite-header-link{margin:0 -8px 0 16px;-webkit-transition:background .2s,color .2s,-webkit-box-shadow .2s;transition:background .2s,color .2s,-webkit-box-shadow .2s;-o-transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s;transition:background .2s,box-shadow .2s,color .2s,-webkit-box-shadow .2s}[dir=rtl] devsite-header .devsite-header-link{margin:0 16px 0 -8px}@media screen and (max-width:840px){devsite-header .devsite-header-link{display:none}}devsite-header .devsite-product-name-wrapper{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0}devsite-header .devsite-product-name-link,devsite-header .devsite-site-logo-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}devsite-header .devsite-product-name-link:focus,devsite-header .devsite-product-name-link:hover,devsite-header .devsite-site-logo-link:focus{opacity:.7;text-decoration:none}devsite-header .devsite-site-logo{height:32px}devsite-header .devsite-has-google-wordmark>.devsite-breadcrumb-link,devsite-header .devsite-has-google-wordmark>.devsite-product-name{direction:ltr}devsite-header .devsite-google-wordmark{height:24px;margin:0 4px 0 0;position:relative;top:5px}devsite-header .devsite-google-wordmark-svg-path{-webkit-transition:fill .2s;-o-transition:fill .2s;transition:fill .2s}devsite-header .devsite-site-logo-link canvas{height:auto!important}devsite-header .devsite-product-logo-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;height:36px;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;width:36px}[dir=ltr] devsite-header .devsite-product-logo-container{margin-right:4px}[dir=rtl] devsite-header .devsite-product-logo-container{margin-left:4px}devsite-header .devsite-product-logo{font-size:32px;height:32px;max-width:32px;min-width:32px;overflow:hidden;white-space:nowrap}devsite-header .devsite-product-logo-container[background] .devsite-product-logo{font-size:28px;height:28px;max-width:28px;min-width:28px}devsite-header .devsite-product-name{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:0;margin:0;max-height:32px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-header .devsite-site-logo:not([src*=\.svg]){height:auto;max-height:32px}devsite-header .devsite-breadcrumb-link>.devsite-product-name{color:inherit}@media screen and (max-width:840px){devsite-header .devsite-product-name-wrapper{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;min-width:0}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item:not(:first-of-type),devsite-header .devsite-product-name-wrapper .devsite-site-logo-link+.devsite-product-name{display:none}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-item,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link,devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-list,devsite-header .devsite-product-name-wrapper .devsite-product-name{width:100%}devsite-header .devsite-product-name-wrapper .devsite-breadcrumb-link{overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis}}devsite-header .devsite-product-id-row{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;min-height:56px;padding:20px 24px 2px}devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row{min-height:72px;padding:20px 24px}devsite-header .devsite-product-description-row{font:400 20px/32px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-header .devsite-breadcrumb-list+.devsite-product-description:not(:empty){margin-top:8px}devsite-header .devsite-product-description{font:16px/24px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0 180px 0 0}[dir=rtl] devsite-header .devsite-product-description{margin:0 0 0 180px}devsite-header .devsite-product-button-row{display:-webkit-box;display:-webkit-flex;display:flex;margin:0 0 0 24px;z-index:1}[dir=rtl] devsite-header .devsite-product-button-row{margin:0 24px 0 0}@media screen and (max-width:840px){devsite-header .devsite-product-id-row{min-height:72px;padding:20px 24px}[dir=rtl] devsite-header .devsite-product-description,devsite-header .devsite-product-description{margin:0}}@media screen and (max-width:600px){devsite-header .devsite-header-no-lower-tabs .devsite-product-id-row,devsite-header .devsite-product-id-row{-webkit-flex-wrap:wrap;flex-wrap:wrap;padding:20px 16px}devsite-header .devsite-product-button-row{-webkit-flex-basis:100%;flex-basis:100%;margin:16px 0 0}}devsite-header[search-expanded] .devsite-header-upper-tabs{display:none}devsite-header[search-expanded] devsite-search{-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0}devsite-header [transition]{-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s}@media screen and (max-width:840px){devsite-header[search-active] .devsite-product-name-wrapper,devsite-header[search-active] devsite-language-selector,devsite-header[search-active] devsite-user{display:none}devsite-header[search-active] devsite-search{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-transform:none!important;-o-transform:none!important;transform:none!important}devsite-header[search-active] .devsite-header-upper-tabs{-webkit-box-flex:0;-webkit-flex:0 1;flex:0 1;overflow:hidden}devsite-header[search-active] .devsite-top-logo-row devsite-search{margin:6px 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox{width:100%}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-searchbox .devsite-search-image{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:-60px;width:-webkit-calc(100vw + 16px);width:calc(100vw + 16px)}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox:before,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-popout{left:auto;right:-60px}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 0 0 16px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button{margin:0 16px 0 0}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-open]{display:none}devsite-header .devsite-top-logo-row devsite-search[search-active] .devsite-search-button[search-close]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header [transition]{-webkit-transition:none;-o-transition:none;transition:none}}devsite-header .devsite-search-background{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-sizing:content-box;box-sizing:content-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:stretch;-webkit-justify-content:stretch;justify-content:stretch;margin:0 0 0 24px;padding:6px 0;pointer-events:none;position:absolute;right:0;-webkit-transform-origin:right center;-o-transform-origin:right center;transform-origin:right center;-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;will-change:transition;z-index:9}[dir=rtl] devsite-header .devsite-search-background{left:0;margin:0 24px 0 0;right:auto;-webkit-transform-origin:left center;-o-transform-origin:left center;transform-origin:left center}devsite-header .devsite-search-background:after{background:#f1f3f4;content:"";-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:100%;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s}devsite-header[billboard] .devsite-search-background{display:none}devsite-header[billboard][bottom-row--hidden] .devsite-search-background{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header[billboard] .devsite-top-logo-row devsite-search .devsite-search-form{opacity:1;-webkit-transition:opacity .2s,-webkit-transform .2s;transition:opacity .2s,-webkit-transform .2s;-o-transition:opacity .2s,-o-transform .2s;transition:opacity .2s,transform .2s;transition:opacity .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-header[billboard][bottom-row--hidden] .devsite-top-logo-row devsite-search .devsite-search-form{-webkit-transform:translateZ(0);transform:translateZ(0)}body[type=error] devsite-header .devsite-top-logo-row .devsite-search-form,devsite-header[billboard]:not([bottom-row--hidden]) .devsite-top-logo-row devsite-search .devsite-search-form{opacity:0;-webkit-transform:translate3d(200px,0,0);transform:translate3d(200px,0,0)}devsite-header[billboard][bottom-row--hidden] .devsite-header-billboard-search devsite-search{opacity:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-left:0}[dir=rtl] devsite-header[billboard] .devsite-header-billboard-search devsite-search{margin-right:0}devsite-header[billboard] .devsite-header-billboard-search devsite-search .devsite-popout{max-height:-webkit-calc(100vh - 255px);max-height:calc(100vh - 255px)}@media screen and (max-width:840px){devsite-header .devsite-top-logo-row devsite-search{width:auto}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox{width:0}devsite-header .devsite-top-logo-row devsite-search .devsite-searchbox .devsite-search-image{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button{-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#5f6368;display:-webkit-box;display:-webkit-flex;display:flex;z-index:1}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]{display:-webkit-box;display:-webkit-flex;display:flex}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]{display:none}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-open]:before{content:"search"}devsite-header .devsite-top-logo-row devsite-search .devsite-search-button[search-close]:before{content:"cancel"}devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:60px;padding-right:8px}[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-item a,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-result-label,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-footer,[dir=rtl] devsite-header .devsite-top-logo-row devsite-search .devsite-suggest-header{padding-left:8px;padding-right:60px}}devsite-header .devsite-header-upper-tabs{-webkit-box-flex:1;-webkit-flex:1 1 0;flex:1 1 0;margin:0 0 0 48px;position:relative;z-index:8}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin:0 48px 0 0}devsite-header devsite-tabs tab a:focus,devsite-header devsite-tabs tab a:hover{text-decoration:none}@media screen and (max-width:840px){devsite-header .devsite-header-upper-tabs{margin-left:0}[dir=rtl] devsite-header .devsite-header-upper-tabs{margin-right:0}devsite-header devsite-tabs.lower-tabs,devsite-header devsite-tabs.upper-tabs{display:none}}devsite-language-selector>devsite-select .devsite-select-toggle{color:#3c4043;max-width:124px;padding:0 31px 0 15px}devsite-progress{pointer-events:none;-webkit-transform-origin:50% 0;-o-transform-origin:50% 0;transform-origin:50% 0;-webkit-transform:scaleY(0);-o-transform:scaleY(0);transform:scaleY(0);-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:-o-transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease,-o-transform .2s ease}devsite-progress[type=indeterminate]{-webkit-transform:scaleY(1);-o-transform:scaleY(1);transform:scaleY(1)}devsite-progress .devsite-progress--indeterminate{position:relative;height:2px}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3,devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{background:#fff;bottom:0;left:0;position:absolute;right:0;top:0;-webkit-transform-origin:0 0;-o-transform-origin:0 0;transform-origin:0 0;-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-1{-webkit-animation:progress-indeterminate-1 2.5s linear infinite;-o-animation:progress-indeterminate-1 2.5s linear infinite;animation:progress-indeterminate-1 2.5s linear infinite;z-index:1}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-2{-webkit-animation:progress-indeterminate-2 2.5s ease-in infinite;-o-animation:progress-indeterminate-2 2.5s ease-in infinite;animation:progress-indeterminate-2 2.5s ease-in infinite;z-index:2}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-3{-webkit-animation:progress-indeterminate-3 2.5s ease-out infinite;-o-animation:progress-indeterminate-3 2.5s ease-out infinite;animation:progress-indeterminate-3 2.5s ease-out infinite;z-index:3}devsite-progress .devsite-progress--indeterminate .devsite-progress--indeterminate-4{-webkit-animation:progress-indeterminate-4 2.5s ease-out infinite;-o-animation:progress-indeterminate-4 2.5s ease-out infinite;animation:progress-indeterminate-4 2.5s ease-out infinite;z-index:4}@-webkit-keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-1{0%{-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-1{0%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}50%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-2{0%,20%{-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-2{0%,20%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}70%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-3{0%,60%{-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-3{0%,60%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}90%,to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}@-webkit-keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@-o-keyframes progress-indeterminate-4{0%,75%{-o-transform:scaleX(0);transform:scaleX(0)}to{-o-transform:scaleX(1);transform:scaleX(1)}}@keyframes progress-indeterminate-4{0%,75%{-webkit-transform:scaleX(0);-o-transform:scaleX(0);transform:scaleX(0)}to{-webkit-transform:scaleX(1);-o-transform:scaleX(1);transform:scaleX(1)}}devsite-search{-webkit-border-radius:2px;border-radius:2px;display:inline-block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;height:36px;margin:6px 0 6px 24px;overflow:hidden;position:relative;text-align:left;-webkit-transform:translateZ(0);transform:translateZ(0);vertical-align:top;width:200px;will-change:transition;z-index:10}[dir=rtl] devsite-search{margin:6px 24px 6px 0;text-align:right}body[pending] devsite-search{visibility:hidden!important}devsite-search .devsite-search-image{color:#5f6368;left:8px;position:absolute;top:6px;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-search .devsite-search-image:before{content:"search"}devsite-search .devsite-search-container{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex}devsite-search .devsite-suggest-results-container{border-top:1px solid #dadce0}devsite-search input.devsite-search-field{background:none;border:0;color:#5f6368;height:36px;outline:0;padding:8px 8px 8px 40px;-webkit-transition:background .2s,color .2s;-o-transition:background .2s,color .2s;transition:background .2s,color .2s;width:100%}[dir=rtl] devsite-search input.devsite-search-field{padding:8px 40px 8px 8px}devsite-search input.devsite-search-field::-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-webkit-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field::-moz-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:-ms-input-placeholder{color:#5f6368;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s}devsite-search input.devsite-search-field:focus{border:0;padding-bottom:8px}devsite-search .devsite-searchbox{width:100%}devsite-search .devsite-searchbox:before{background:#fff;content:"";height:500px;left:-6px;opacity:0;pointer-events:none;position:absolute;top:-458px;-webkit-transition:opacity 1ms .2s;-o-transition:opacity 1ms .2s;transition:opacity 1ms .2s;width:-webkit-calc(100% + 12px);width:calc(100% + 12px);will-change:opacity;z-index:-1}[dir=rtl] devsite-search .devsite-searchbox:before{left:auto;right:-6px}devsite-search[search-active]{overflow:visible}devsite-search[search-active] .devsite-searchbox:before{opacity:1}devsite-search[search-active] .devsite-searchbox:hover{background:#f1f3f4}devsite-search[search-active] .devsite-search-field{color:#202124}devsite-search[search-active] .devsite-search-field::-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-webkit-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field::-moz-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-field:-ms-input-placeholder{color:#5f6368}devsite-search[search-active] .devsite-search-image{color:#5f6368}devsite-search .devsite-popout{display:block;margin-top:6px;position:absolute;-webkit-transform:translateY(-100vh);-o-transform:translateY(-100vh);transform:translateY(-100vh);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;visibility:hidden;width:100%;z-index:-2}devsite-search[search-active] .devsite-popout{display:block;-webkit-transform:translateY(0);-o-transform:translateY(0);transform:translateY(0);-webkit-transition-delay:.2s;-o-transition-delay:.2s;transition-delay:.2s;visibility:visible;will-change:transform}devsite-search .devsite-popout-result{max-height:-webkit-calc(100vh - 56px);max-height:calc(100vh - 56px);overflow-y:auto;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}devsite-search .devsite-popout-result:empty,devsite-search[search-active][no-suggest] .devsite-popout{display:none}devsite-search .devsite-suggest-wrapper{padding:16px 0 0;font-size:14px}devsite-search .devsite-result-item,devsite-search .devsite-result-label{font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0}devsite-search .devsite-result-label{padding-left:40px}[dir=rtl] devsite-search .devsite-result-label{padding-left:0;padding-right:40px}devsite-search .devsite-result-item a{color:#202124;display:block;outline:0;padding:8px;text-decoration:none;-webkit-transition:background .2s;-o-transition:background .2s;transition:background .2s;will-change:transition}[dir=ltr] devsite-search .devsite-result-item a{padding-left:40px}[dir=rtl] devsite-search .devsite-result-item a{padding-right:40px}devsite-search .devsite-result-item.highlight a,devsite-search .devsite-result-item a:focus,devsite-search .devsite-result-item a:hover{background:#f1f3f4}devsite-search .devsite-result-item b{font-weight:500}devsite-search .devsite-suggest-footer{border-top:1px solid #dadce0;margin:8px 0 0;padding:7px 0 8px 40px}[dir=rtl] devsite-search .devsite-suggest-footer{padding:7px 40px 8px 0}devsite-search .devsite-suggest-footer>.button{display:inline-block;margin:6px 0;max-width:-webkit-calc(100% - 16px);max-width:calc(100% - 16px)}[dir=ltr] devsite-search .devsite-suggest-footer>.button{margin-right:16px}[dir=rtl] devsite-search .devsite-suggest-footer>.button{margin-left:16px}devsite-search .devsite-suggest-footer>.button-white{max-width:100%}[dir=ltr] devsite-search .devsite-suggest-footer>.button-white{margin-left:-8px}[dir=rtl] devsite-search .devsite-suggest-footer>.button-white{margin-right:-8px}devsite-search .devsite-suggest-header{font:500 11px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.8px;margin:12px 0;padding-left:40px;text-transform:uppercase}[dir=rtl] devsite-search .devsite-suggest-header{padding-left:0;padding-right:40px}devsite-search hr+.devsite-suggest-header{margin-top:24px}devsite-search .devsite-suggest-header .devsite-suggest-project:before{content:"|";margin:0 8px}devsite-search hr{background:#ddd;margin:8px 0}devsite-search .devsite-suggestion-fragment+.devsite-suggestion-fragment:before{content:"|";margin:0 8px}devsite-search .devsite-search-disabled{padding-bottom:16px}devsite-search[compact]{width:auto}devsite-search[compact] input.devsite-search-field{width:0}devsite-search[compact] .devsite-search-image{left:-webkit-calc(50% - 12px);left:calc(50% - 12px);pointer-events:none}[dir=ltr] devsite-search[compact][search-active] .devsite-search-image{left:8px;right:auto}[dir=rtl] devsite-search[compact][search-active] .devsite-search-image{right:8px;left:auto}devsite-search[compact][search-active] input.devsite-search-field{width:100%}body[theme] devsite-search[compact] .devsite-search-field,body[theme] devsite-search[compact] .devsite-searchbox{background-color:transparent}@media screen and (max-width:840px){devsite-search input.devsite-search-field{padding-left:40px}[dir=rtl] devsite-search input.devsite-search-field{padding-left:0;padding-right:40px}.devsite-search-background,.devsite-search-background:after,[search-active] .devsite-search-background:after,devsite-search .devsite-search-field,devsite-search .devsite-search-field:hover{-webkit-transition:none;-o-transition:none;transition:none}devsite-search .devsite-search-image{left:8px}[dir=rtl] devsite-search .devsite-search-image{left:auto;right:8px}devsite-header devsite-search{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-left:8px;overflow:visible}[dir=rtl] devsite-header devsite-search{margin-left:0;margin-right:8px}devsite-header devsite-search .devsite-search-form{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}devsite-header .devsite-top-logo-row devsite-search:not([search-active]) input.devsite-search-field{padding:0}}devsite-select{display:inline-block;position:relative}devsite-select+devsite-select{margin:0 0 0 16px}devsite-select select{display:none!important;pointer-events:none!important;position:absolute;z-index:-1}devsite-select .devsite-select{position:relative}devsite-select .devsite-select-toggle{border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;padding:0 27px 0 7px;-moz-appearance:none;-webkit-appearance:none;background:#fff url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23212121'/></svg>") no-repeat 100%;-webkit-box-shadow:none;box-shadow:none;color:#202124;cursor:pointer;display:inline-block;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:36px;line-height:34px;max-width:256px;min-width:72px;outline:0;overflow:hidden;text-align:left;text-indent:.01px;-o-text-overflow:ellipsis;text-overflow:ellipsis;-webkit-transition:background-color .2s;-o-transition:background-color .2s;transition:background-color .2s;vertical-align:middle;white-space:nowrap}devsite-select .devsite-select-toggle:focus,devsite-select .devsite-select-toggle:hover{background-color:#f1f3f4}devsite-select .devsite-select-toggle:active{background-color:#e8eaed}devsite-select .devsite-select-toggle:disabled{background:#f1f3f4 url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='20' height='4' viewBox='0 0 20 4'><path d='M0,0l4,4l4-4H0z' fill='%23bdbdbd'/></svg>") no-repeat 100%;border-color:transparent;color:#bdc1c6;cursor:default}devsite-select .devsite-select-list{background:#fff;border:1px solid #e8eaed;-webkit-border-radius:2px;border-radius:2px;display:none;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);font:400 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;max-height:304px;opacity:0;outline:0;overflow-y:auto;padding:8px 0;pointer-events:none;position:absolute;-webkit-transition:opacity .2s,visibility .2s;-o-transition:opacity .2s,visibility .2s;transition:opacity .2s,visibility .2s;z-index:1015}devsite-select[menu--open] .devsite-select-list{display:block;pointer-events:auto}devsite-select[menu--show] .devsite-select-list{opacity:1}devsite-select[menu-position=above] .devsite-select-list{bottom:36px}devsite-select[menu-position=below] .devsite-select-list{top:36px}devsite-select .devsite-select-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin:0;min-height:48px;min-width:100%;padding:8px 16px;white-space:nowrap}devsite-select .devsite-select-item.devsite-focused,devsite-select .devsite-select-item:focus,devsite-select .devsite-select-item:hover{background-color:#f1f3f4;cursor:pointer}devsite-select .devsite-select-item[data-selected]{background-color:#f1f3f4;font-weight:500}devsite-select.devsite-select--multiple .devsite-select-item{padding-left:48px;position:relative}devsite-select.devsite-select--multiple .devsite-select-item:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;color:#80868b;content:"check_box_outline_blank";display:block;font-size:24px;left:16px;position:absolute;top:50%;-webkit-transform:translateY(-50%);-o-transform:translateY(-50%);transform:translateY(-50%)}devsite-select.devsite-select--multiple .devsite-select-item[data-selected]:before{color:#1976d2;content:"check_box"}@media screen and (max-width:600px){devsite-select{display:block}devsite-select+devsite-select{margin:16px 0 0}}devsite-sitemask{background:rgba(0,0,0,.4);bottom:-200px;cursor:pointer;left:-200px;opacity:0;pointer-events:none;position:fixed;right:-200px;top:-200px;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;-o-transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;transition:opacity .2s cubic-bezier(.4,0,.2,1),visibility .2s linear;visibility:hidden;z-index:1012;-webkit-tap-highlight-color:transparent}devsite-sitemask[visible]{opacity:1;pointer-events:auto;-webkit-transition:opacity .2s ease;-o-transition:opacity .2s ease;transition:opacity .2s ease;visibility:visible}devsite-tabs{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px;max-width:-webkit-calc(100% - 208px);max-width:calc(100% - 208px);position:relative}devsite-tabs[connected]{max-width:none}devsite-tabs .devsite-tabs-wrapper{bottom:0;display:-webkit-box;display:-webkit-flex;display:flex;left:0;overflow:hidden;position:absolute;right:0;top:0}devsite-tabs[no-overflow] .devsite-tabs-wrapper{overflow:auto}devsite-tabs[dropdown--open] .devsite-tabs-wrapper,devsite-tabs[overflow-menu--open] .devsite-tabs-wrapper{overflow:visible}devsite-tabs tab{-webkit-flex-shrink:0;flex-shrink:0;position:relative}devsite-tabs tab,devsite-tabs tab>a{display:-webkit-box;display:-webkit-flex;display:flex}devsite-tabs tab>a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;font:500 14px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:0;padding:0 24px;text-transform:uppercase;-webkit-transition:color .2s;-o-transition:color .2s;transition:color .2s;white-space:nowrap}devsite-tabs tab>a,devsite-tabs tab>a:focus,devsite-tabs tab>a:hover{text-decoration:none}devsite-tabs.upper-tabs tab a{font-weight:400;text-transform:none}devsite-tabs.upper-tabs tab[active]>a{font-weight:500}devsite-tabs tab[active] a:after,devsite-tabs tab a:focus:after,devsite-tabs tab a:hover:after{bottom:0;content:"";display:block;height:2px;left:0;position:absolute;right:0}devsite-tabs tab[dropdown]>a{padding:0 0 0 24px;position:relative;z-index:2}[dir=rtl] devsite-tabs tab[dropdown]>a{padding:0 24px 0 0}devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{-webkit-box-align:center;-webkit-align-items:center;align-items:center;cursor:pointer;display:-webkit-box;display:-webkit-flex;display:flex}[dir=rtl] devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle{padding:0}devsite-tabs.upper-tabs .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(0deg);-o-transform:rotate(0deg);transform:rotate(0deg);-webkit-transition:color .2s,-webkit-transform .2s;transition:color .2s,-webkit-transform .2s;-o-transition:color .2s,-o-transform .2s;transition:color .2s,transform .2s;transition:color .2s,transform .2s,-webkit-transform .2s,-o-transform .2s}devsite-tabs.upper-tabs[overflow-menu--open] tab:hover .devsite-icon-arrow-drop-down:before,devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-toggle:before{-webkit-transform:rotate(-180deg);-o-transform:rotate(-180deg);transform:rotate(-180deg)}devsite-tabs tab[overflow-tab]{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative}devsite-tabs tab[overflow-tab][collapsed]{-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0}devsite-tabs.upper-tabs tab[overflow-tab]:after{content:"";height:48px;position:absolute;z-index:-1}[dir=ltr] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-6px;right:-100%}[dir=rtl] devsite-tabs.upper-tabs tab[overflow-tab]:after{left:-100%;right:-6px}devsite-tabs tab[overflow-tab] tab>a{padding:0 24px}devsite-tabs tab[overflow-tab] a{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;position:relative}devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown,devsite-tabs tab[overflow-tab] tab .devsite-tabs-dropdown-toggle{display:none}devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{background:#fff;-webkit-border-radius:2px;border-radius:2px;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;max-height:320px;overflow-y:auto;padding:16px 0;position:absolute;top:-16px;z-index:1005}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu{-webkit-border-radius:0 0 2px 2px;border-radius:0 0 2px 2px;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);top:48px;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;-o-transition:-o-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s,-o-transform .2s;z-index:-1}devsite-tabs.upper-tabs .devsite-tabs-overflow-menu .devsite-tabs-dropdown-toggle{display:none!important}devsite-tabs.upper-tabs tab[overflow-tab] .devsite-tabs-overflow-menu[hidden]{display:block!important;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0)}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a{background:#fff;color:#5f6368;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;height:48px}body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:focus,body devsite-tabs tab[overflow-tab] .devsite-tabs-overflow-menu tab a:hover{background:#f1f3f4;color:#202124}devsite-tabs .devsite-tabs-dropdown{display:block;font-size:13px;left:-6px;min-width:-webkit-calc(100% + 12px);min-width:calc(100% + 12px);outline:0;overflow:hidden;padding:0 6px 6px;pointer-events:none;position:absolute;top:100%;z-index:-1}[dir=rtl] devsite-tabs .devsite-tabs-dropdown{right:-6px;left:auto}devsite-tabs [dropdown-full] .devsite-tabs-dropdown{left:0;padding:0 0 6px;width:100vw}devsite-tabs .devsite-tabs-dropdown-content{background-color:#fff;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15),inset 0 4px 6px -4px rgba(154,160,166,.5);overflow:auto;max-height:600px;max-width:100vw;padding:0 12px;pointer-events:none;-webkit-transform:translate3d(0,-150%,0);transform:translate3d(0,-150%,0);-webkit-transition:-webkit-transform 0s;transition:-webkit-transform 0s;-o-transition:-o-transform 0s;transition:transform 0s;transition:transform 0s,-webkit-transform 0s,-o-transform 0s;white-space:nowrap}devsite-tabs .devsite-tabs-dropdown[dropdown-transition] .devsite-tabs-dropdown-content{-webkit-transition:-webkit-transform .5s;transition:-webkit-transform .5s;-o-transition:-o-transform .5s;transition:transform .5s;transition:transform .5s,-webkit-transform .5s,-o-transform .5s}devsite-tabs tab[dropdown--open] .devsite-tabs-dropdown-content{pointer-events:all;-webkit-transform:translateZ(0);transform:translateZ(0)}devsite-tabs [dropdown-full] .devsite-tabs-dropdown-column{-webkit-box-flex:1;-webkit-flex:1;flex:1;min-width:0}devsite-tabs .devsite-tabs-dropdown-section{list-style:none;padding:0 12px}devsite-tabs .devsite-tabs-dropdown-section:first-child{margin-top:18px}devsite-tabs .devsite-tabs-dropdown-section:not(:first-child){margin-top:54px}devsite-tabs tab[dropdown] .devsite-nav-item,devsite-tabs tab[dropdown] .devsite-nav-title{line-height:18px;margin:0 0 18px}devsite-tabs tab[dropdown] .devsite-nav-title{color:#5f6368;font-weight:700;padding:0}devsite-tabs [dropdown-full] .devsite-nav-item>a,devsite-tabs [dropdown-full] .devsite-nav-title{display:block;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}devsite-tabs tab[dropdown] .devsite-nav-item-description{color:#5f6368;white-space:normal}body[theme] devsite-tabs .devsite-tabs-dropdown a,body[theme] devsite-tabs .devsite-tabs-dropdown a:visited{color:#202124;display:block;font-weight:400}body[theme] devsite-tabs .devsite-tabs-dropdown a:focus,body[theme] devsite-tabs .devsite-tabs-dropdown a:hover{color:#1a73e8}devsite-tabs[render-hidden]{width:100%}devsite-tabs[render-hidden] tab[overflow-tab],devsite-tabs tab[overflow-tab][render-hidden]{-webkit-box-flex:0;-webkit-flex:none;flex:none}devsite-tabs tab[dropdown] .devsite-tabs-close-button{color:#202124;cursor:pointer;position:absolute;right:24px;top:24px;visibility:hidden;z-index:1}devsite-tabs tab[dropdown] .devsite-tabs-close-button:focus,devsite-tabs tab[dropdown] .devsite-tabs-close-button:hover{color:#1a73e8}devsite-tabs tab[dropdown--open] .devsite-tabs-close-button{visibility:visible}devsite-toc.devsite-toc{float:right;width:160px}[dir=rtl] devsite-toc.devsite-toc{float:left}devsite-toc>.devsite-nav-list{border-left:4px solid #5f6368;width:160px}[dir=rtl] devsite-toc>.devsite-nav-list{border-left:0;border-right:4px solid #5f6368}devsite-toc[fixed]>.devsite-nav-list{contain:content;overflow-x:hidden;overflow-y:auto;position:fixed;-webkit-transform:translateZ(0);transform:translateZ(0);will-change:max-height,transform}[dir=ltr] devsite-toc[fixed]>.devsite-nav-list{padding-right:8px}[dir=rtl] devsite-toc[fixed]>.devsite-nav-list{padding-left:8px}devsite-toc>.devsite-nav-list>:first-child>.devsite-nav-title{padding-top:0}devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-list>:last-child>.devsite-nav-title:last-child,devsite-toc>.devsite-nav-list>:last-child>.devsite-nav-title:only-child{padding-bottom:0}devsite-toc.devsite-toc-embedded{display:none}devsite-toc.devsite-toc-embedded>.devsite-nav-list{width:auto}devsite-toc .devsite-nav-list{padding:0 0 0 12px}[dir=rtl] devsite-toc .devsite-nav-list{padding:0 12px 0 0}devsite-toc .devsite-nav-more-items,devsite-toc .devsite-nav-show-all{display:none}devsite-toc[expandable] .devsite-nav-more-items,devsite-toc[expandable] .devsite-nav-show-all{color:#5f6368;display:block;height:24px;padding:0}devsite-toc .devsite-nav-show-all{margin:-4px 0 0 4px;min-width:20px}devsite-toc .devsite-nav-show-all:before{content:"expand_more"}devsite-toc .devsite-nav-more-items{margin-bottom:-8px;min-width:0}devsite-toc .devsite-nav-more-items:before{content:"more_horiz"}devsite-toc[expanded] .devsite-nav-more-items:before,devsite-toc[expanded] .devsite-nav-show-all:before{content:"expand_less"}devsite-toc .devsite-toc-toggle{display:-webkit-box;display:-webkit-flex;display:flex;margin:0}devsite-toc .devsite-show-apix{margin-top:12px}@media screen and (max-width:1252px){devsite-toc.devsite-toc,devsite-toc[visible].devsite-toc{display:none}devsite-toc.devsite-toc-embedded:not(:empty){display:block;margin:20px 0 24px}body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 40px 24px}}@media screen and (max-width:840px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 24px 24px}}@media screen and (max-width:600px){body[type=landing] devsite-toc.devsite-toc-embedded:not(:empty){margin:20px 16px 24px}}@charset "UTF-8";devsite-user{display:block;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto}devsite-user:not(:empty){margin:0 -8px 0 0;min-width:60px}[dir=rtl] devsite-user:not(:empty){margin:0 0 0 -8px}devsite-user #devsite-signin-btn{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/36px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user devsite-spinner{margin:4px 8px 4px 20px}devsite-user devsite-spinner.hide{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease;-webkit-animation-delay:.45s;-o-animation-delay:.45s;animation-delay:.45s}devsite-user .ogb-wrapper{display:-webkit-box;display:-webkit-flex;display:flex;opacity:1;-webkit-transition:opacity .2s cubic-bezier(.4,0,.2,1);-o-transition:opacity .2s cubic-bezier(.4,0,.2,1);transition:opacity .2s cubic-bezier(.4,0,.2,1)}devsite-user .ogb-pending{opacity:0}devsite-user .ogb-si{margin:0 0 0 16px}[dir=rtl] devsite-user .ogb-si{margin:0 16px 0 0}devsite-user .ogb-so{margin:0 0 0 12px}[dir=rtl] devsite-user .ogb-so{margin:0 12px 0 0}devsite-user .gb_Sb>.gb_Rb{-webkit-box-sizing:content-box;box-sizing:content-box}devsite-user button.devsite-user-change-account,devsite-user button.devsite-user-signout{height:auto;color:#3c4043}devsite-user button.devsite-user-change-account .material-icons,devsite-user button.devsite-user-signout .material-icons{margin:0;height:auto;width:auto;top:auto}devsite-user button.devsite-user-change-account{border:0}@media (-o-min-device-pixel-ratio:5/4),(-webkit-min-device-pixel-ratio:1.25),(min-resolution:1.25dppx),not all{[dir=rtl] devsite-user .gb_xa:before{-webkit-transform-origin:right 0;-o-transform-origin:right 0;transform-origin:right 0}}devsite-user .devsite-user-dialog{display:none}devsite-user .devsite-user-dialog a:link,devsite-user .devsite-user-dialog a:visited{text-decoration:none}devsite-user[dialog--open] .devsite-user-dialog{background:#fff;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:8px;border-radius:8px;-webkit-box-shadow:0 2px 10px rgba(0,0,0,.2);box-shadow:0 2px 10px rgba(0,0,0,.2);color:#000;display:block;max-height:-webkit-calc(100vh - 86px);max-height:calc(100vh - 86px);outline:none;overflow:auto;position:absolute;right:24px;top:62px;width:354px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:24px;right:auto}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-toggle,devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-border-radius:50%;border-radius:50%;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;margin:0;overflow:hidden;padding:0}devsite-user .devsite-user-dialog-toggle{border:4px solid transparent;-webkit-box-sizing:content-box;box-sizing:content-box;height:32px;margin:0 4px 0 0;overflow:hidden;text-decoration:none;width:32px}[dir=rtl] devsite-user .devsite-user-dialog-toggle{margin:0 0 0 4px}devsite-user .devsite-user-dialog-toggle:focus{border-color:rgba(0,0,0,.2)!important}devsite-user .devsite-user-dialog-photo-thumbnail{height:32px;width:32px}devsite-user[js-signin] button{-webkit-box-shadow:none;box-shadow:none}devsite-user[js-signin] .devsite-user-dialog-toggle{opacity:0;-webkit-transition:opacity .45s ease;-o-transition:opacity .45s ease;transition:opacity .45s ease}devsite-user[js-signin] .devsite-user-dialog-toggle.show{opacity:1}devsite-user .devsite-user-dialog-toggle .devsite-user-dialog-letter{-webkit-box-flex:0;-webkit-flex:0 0 32px;flex:0 0 32px;font-size:17px;height:32px}devsite-user .devsite-user-dialog-learn-more{background-color:#e8f0fe;-webkit-border-radius:4px;border-radius:4px;color:#5f6368;font:12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:4px 4px 0;padding:4px 29px;text-align:center}devsite-user .devsite-user-dialog-learn-more a,devsite-user .devsite-user-dialog-learn-more span{font-weight:500}devsite-user .devsite-user-dialog-learn-more a{color:#1a73e8}devsite-user .devsite-user-dialog-learn-more a:focus,devsite-user .devsite-user-dialog-learn-more a:hover{text-decoration:underline}devsite-user .devsite-user-dialog-user{padding:20px 33px 23px;text-align:center}devsite-user .devsite-user-dialog-photo{margin:0 auto 16px;position:relative;left:-2px}devsite-user .devsite-user-dialog-photo,devsite-user .devsite-user-dialog-photo-portrait{height:80px;width:80px}devsite-user .devsite-user-dialog-letter{text-transform:uppercase}devsite-user .devsite-user-dialog-photo .devsite-user-dialog-letter{font-size:52px}devsite-user .devsite-user-dialog-email,devsite-user .devsite-user-dialog-name{-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden}devsite-user .devsite-user-dialog-name{color:#202124;font:500 16px/22px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.29px}devsite-user .devsite-user-dialog-email{color:#5f6368;font:400 14px/19px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}devsite-user .devsite-user-manage{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;border:1px solid #dadce0;-webkit-border-radius:17px;border-radius:17px;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;font:500 14px/20px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;height:34px;letter-spacing:.25px;margin:16px 0 0;padding:0 16px;white-space:nowrap}devsite-user .devsite-user-manage:link,devsite-user .devsite-user-manage:visited{color:#3c4043}devsite-user .devsite-user-manage:focus,devsite-user .devsite-user-manage:hover{background-color:#f8f9fa}devsite-user .devsite-user-manage:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-buttons{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}devsite-user .devsite-user-dialog .developer-profile:link,devsite-user .devsite-user-dialog .developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-developer-profile:link,devsite-user .devsite-user-dialog .devsite-user-developer-profile:visited,devsite-user .devsite-user-dialog .devsite-user-signin:link,devsite-user .devsite-user-dialog .devsite-user-signin:visited,devsite-user .devsite-user-signout:link,devsite-user .devsite-user-signout:visited{color:#3c4043}devsite-user .devsite-user-dialog .developer-profile:focus,devsite-user .devsite-user-dialog .developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-developer-profile:focus,devsite-user .devsite-user-dialog .devsite-user-developer-profile:hover,devsite-user .devsite-user-dialog .devsite-user-signin:focus,devsite-user .devsite-user-dialog .devsite-user-signin:hover,devsite-user .devsite-user-signout:focus,devsite-user .devsite-user-signout:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog .developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-developer-profile:active,devsite-user .devsite-user-dialog .devsite-user-signin:active,devsite-user .devsite-user-signout:active{background-color:#e8eaed}devsite-user .devsite-user-dialog .devsite-user-developer-profile,devsite-user .devsite-user-dialog .devsite-user-signin{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.25px;padding:15px 39px 16px;width:100%}devsite-user .devsite-user-dialog-buttons>:first-child{border-top:1px solid #e8eaed}devsite-user .devsite-user-dialog .devsite-user-signin{border-bottom:1px solid #e8eaed}devsite-user .devsite-user-dialog .new-notification{background:#1967d2;-webkit-border-radius:10px;border-radius:10px;color:#fff;font-weight:700;font-size:12px;letter-spacing:.3px;padding:2px 8px}[dir=ltr] devsite-user .devsite-user-dialog .new-notification{margin-left:12px}[dir=ltr] devsite-user .devsite-user-signin .devsite-switch-account-icon,[dir=rtl] devsite-user .devsite-user-dialog .new-notification{margin-right:12px}[dir=rtl] devsite-user .devsite-user-signin .devsite-switch-account-icon{margin-left:12px}devsite-user .devsite-user-developer-profile .google-dev-icon{width:28px;position:relative}[dir=ltr] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:-4px;margin-right:8px}[dir=rtl] devsite-user .devsite-user-developer-profile .google-dev-icon{margin-left:8px;margin-right:-4px}devsite-user .devsite-user-signout{border:1px solid #dadce0;-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:500 14px/16px Google Sans,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.15px;margin:16px auto;padding:10px 24px}devsite-user .devsite-user-signout:active{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background-color:#e8eaed;border-color:transparent}devsite-user .devsite-user-dialog-footer{border-top:1px solid #e8eaed;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;padding:14px 20px}devsite-user .devsite-user-dialog-footer-link{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;margin:0}devsite-user .devsite-user-dialog-footer-link:not(:first-child):before{color:#5f6368;content:"•";font-size:13px}devsite-user .devsite-user-dialog-footer-link>a{-webkit-border-radius:4px;border-radius:4px;display:inline-block;font:400 12px/16px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;padding:4px 8px}devsite-user .devsite-user-dialog-footer-link>a:link,devsite-user .devsite-user-dialog-footer-link>a:visited{color:#5f6368}devsite-user .devsite-user-dialog-footer-link>a:focus,devsite-user .devsite-user-dialog-footer-link>a:hover{background-color:#f8f9fa}devsite-user .devsite-user-dialog-footer-link>a:active{background-color:#e8eaed}@media screen and (max-width:840px){devsite-user .ogb-si{margin:0 0 0 4px}[dir=rtl] devsite-user .ogb-si{margin:0 4px 0 0}devsite-user[dialog--open] .devsite-user-dialog{right:16px}[dir=rtl] devsite-user[dialog--open] .devsite-user-dialog{left:16px;right:auto}}devsite-content-footer{clear:both;color:rgba(0,0,0,.65);display:block;font:13px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif}google-codelab-step{line-height:24px;display:block}google-codelab-step:focus{outline:none}google-codelab-step code,google-codelab-step pre{font-family:Source Code Pro,Helvetica,Arial;font-size:inherit;-webkit-border-radius:4px;border-radius:4px;overflow-x:auto;overflow-y:visible}google-codelab-step code{background-color:#e8eaed;padding:.1em .3em}google-codelab-step pre{display:block;color:#fff;background-color:#28323f;padding:14px;-webkit-text-size-adjust:none;line-height:1.4}google-codelab-step pre>code{padding:0;background-color:transparent}google-codelab-step code em{color:#97c8f2}google-codelab-step code .str,google-codelab-step pre .str{color:#34a853}google-codelab-step code .kwd,google-codelab-step pre .kwd{color:#f538a0}google-codelab-step code .com,google-codelab-step pre .com{color:#bdc1c6;font-style:italic}google-codelab-step code .typ,google-codelab-step pre .typ{color:#24c1e0}google-codelab-step code .lit,google-codelab-step pre .lit{color:#4285f4}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:#f8f9fa}google-codelab-step code .tag,google-codelab-step pre .tag{color:#24c1e0}google-codelab-step code .atn,google-codelab-step pre .atn{color:#eda912}google-codelab-step code .atv,google-codelab-step pre .atv{color:#34a853}google-codelab-step code .dec,google-codelab-step pre .dec{color:#5195ea}google-codelab-step paper-button{display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:5.14em;margin:0 .29em;background:transparent;-webkit-tap-highlight-color:transparent;font:inherit;text-transform:uppercase;outline-width:0;-webkit-border-radius:3px;border-radius:3px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:pointer;z-index:0;padding:.7em .57em;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)}google-codelab-step h2.step-title{font-family:Google Sans,Arial,sans-serif!important;font-size:28px!important;font-weight:400!important;line-height:1em!important;margin:0 0 30px!important}google-codelab:not([theme=minimal]) google-codelab-step .instructions{-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);background:#fff;max-width:800px;font-size:14px;margin:0 auto 90px;-webkit-border-radius:4px;border-radius:4px}google-codelab-step .instructions .inner{padding:24px}google-codelab[theme=minimal] google-codelab-step .instructions .inner{padding:0 24px}@media (max-width:800px){google-codelab .instructions{margin:0 0 16px}}google-codelab:not([theme=minimal]) google-codelab-step .instructions a,google-codelab:not([theme=minimal]) google-codelab-step .instructions a:visited{color:#1a73e8}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2,google-codelab:not([theme=minimal]) google-codelab-step .instructions h3,google-codelab:not([theme=minimal]) google-codelab-step .instructions h4{font-weight:400;margin:0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:300;line-height:1em;font-size:22px}google-codelab:not([theme=minimal]) google-codelab-step .instructions{line-height:24px}google-codelab:not([theme=minimal]) google-codelab-step .instructions li{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions h2{font-weight:500;margin:20px 0 0;font-size:20px}google-codelab:not([theme=minimal]) google-codelab-step .instructions h3{font-weight:500;margin:20px 0 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside{padding:.5em 1em;margin:2em 0;border-left:4px solid;-webkit-border-radius:4px;border-radius:4px}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside p{margin:.5em 0}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.note,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.notice{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.special,google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.tip{border-color:#137333;background:#e6f4ea;color:#212124}google-codelab:not([theme=minimal]) google-codelab-step .instructions aside.warning{border-color:#ea8600;background:#fef7e0;color:#212124}google-codelab-step .instructions aside.callout{background-color:#e8f0fe;margin:20px 0;padding:15px;border-left:3px solid #185abc;-webkit-border-radius:4px;border-radius:4px;color:#212124;font-size:14px;line-height:1.5}google-codelab-step aside.callout b{color:#185abc}google-codelab-step .instructions ul.checklist{list-style:none;padding:0 0 0 1em}google-codelab-step .instructions ::content ul.checklist li,google-codelab-step .instructions ul.checklist li{padding-left:24px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAWlBMVEUAAAAxokwwoks1pFAxokwxokwxokwxokwxokwnnkQnnkQnnkRou3y84cTS69cxokwonkQxokwnnkRqvH1VsmtluXlVsmsnnkRdtnLw+PIxokwqn0YinEAfmj3goh/UAAAAGnRSTlMA2CcEo+6AQT7+2IOBJxPl27alhoBnX15SCCe258UAAAB+SURBVEjH7dA5EoAgEERR3BcQ923Q+1/T0SqKlNbMouP3gxkRFvZpyQb64VSQT4mOcYc8mU5DnqIG8zXoozj4d34tML+YrET8XBFx4e2F4oAL4N7J3EUB/EfSUwD/zG3hvFdROu9XtL31vgXguQA9F6DnAvM8WbOHpkXYD3cBBCcPjtASYjwAAAAASUVORK5CYII=")}google-codelab-step .instructions h2 code,google-codelab-step .instructions table code{background:#fff}google-codelab-step .instructions .indented{margin-left:40px}google-codelab-step .instructions strong{font-weight:600}google-codelab-step .instructions :link paper-button{text-decoration:none!important}google-codelab-step .instructions paper-button{display:inline-block;-webkit-border-radius:4px;border-radius:4px;color:#fff;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;padding:6px 16px 6px 12px;text-transform:none}google-codelab-step .instructions paper-button a{text-decoration:none;color:inherit!important}google-codelab-step a paper-button{display:inline-block}google-codelab-step .instructions paper-button.colored{background-color:#1e8e3e}google-codelab-step .instructions paper-button.red{background-color:#d93025}google-codelab-step .instructions iron-icon{vertical-align:sub;margin-right:7px;margin-left:3px;font-size:16px;top:-1px;position:relative}google-codelab-step .instructions img{max-width:100%;vertical-align:bottom}google-codelab-step .instructions .image-container{text-align:center}google-codelab-step .instructions table{border-spacing:0}google-codelab-step .instructions td{vertical-align:top;border-bottom:1px solid #ccc;padding:8px}google-codelab-step .instructions table p{margin:0}google-codelab:not([theme=minimal]) .instructions h3.faq{border-bottom:1px solid #ddd}google-codelab:not([theme=minimal]) .instructions ul.faq{list-style:none;padding-left:1em}google-codelab:not([theme=minimal]) .instructions .faq li{font-size:1.1em;margin-bottom:.8em}google-codelab:not([theme=minimal]) .instructions .faq a{color:inherit;text-decoration:none}google-codelab:not([theme=minimal]) .instructions .faq a:hover{text-decoration:underline}google-codelab-step .instructions .faq a[href*=cloud\.google\.com]{padding-left:22px;-o-background-size:20px;background-size:20px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAC9FBMVEX////u7u7v7+/ZRDf/zEH/zkPbRTlJifT/z0XYRDhRj/XWRDdPjvVOjfX/zkH/z0fXQzdGiPXUQzdMi/VKi/Xr6+3t7e1SkPXbRDdLjPW70fPaQjVOjPXx8PD9/f1nnff19fXRQzdHifVJhOxEh/Tu7/JLifH9y0DUQjVtofhOi/NKh+9HgelGf+b+7saaWXX/zDz+yTrZRzrRQjT7+/vxx8PeqkTsuj32uzrOQzdqn/j39/fr6Oj9yT/3vjvaQDPUPTBJivVKifPq7O/z0Mz968T4wj7zwD3NPTHGPDD0+P9Bifrv8/Xx8vJJhu5EfOPo3Nv/0Ez8y0rxvT3ZPjBDi/tBhPT55ePkysj+3Hr/0VLHVErcpkPcTUH7xD35wTzztznpsTjKRTjo8P3U4/z6+vr//PmQt/mBrff//fayy/ZUkfZjmfSXufP78/L18+//+Oj66Of+9eBCdt3t59f/89TvxcPsurXasKztqKL+4Y7jjIT0033ShH3hc2ngaV7/1F3/0ljjv1fbWU37xkDaoEDCSj/xujvptjvutjrxtjn8xDflrDb5vjXJQjX2tzP4+v+bwf3I3Pyiw/t0pvhel/VZlPW90fP/+/DV3/Cdu+/e4uv33Nk+btP+8Mz+8Mniwb7+7L3u371Vdrv+6bDusq3+56j+5aTx2Z+Gjpj+4pbolI3RjYehnIT+2XPedW370mbHaGDMZl3bY1jQVUvbU0f/00PYTkLKSj/tsjjwpjfeXjfnfjbEQjbXPjTSPS/w9vu50vs7hfr5+flypPf89vb89fTF1fHM2vBsnfB3ou/67+6swOpekert6uNdi+M5deLq4OA/dt83bt80a9xmjdby19VKc8jx5cf12cP/7r/gwr+pnr/v4b7xwLtffbf+6rHx3KnorKjYqKTXpqHXo57aop3ZkovTwIboh3+jX3f30XDgenCWUm3Kc2zIsGnMcGj4y1zgYFXgX1TfrEXWlz7mhjfsrTbkljbabDbcVTbsmTBDSJ02AAAFFklEQVRYw+2WV1jTUABGkyZpi6W0tKUWC0hBsAporbMWrCgqKA6WCooK7r333nvvvffee++99957b33x3uTetIEGnnzx83y89Tunf25SWuI/f4Oqk6cU4ijLkr9sfp5Ckyfk6h8o5JkH4umpVPr5+fnqdLro6GiZTBYkk3l71z5SNbdA8zyt8wLYhKefUunr66vV6WQsMDExF3/StLytka/0VPopfbUgoEWBoCBv76lNcj6AFnnQ+6MB0BcsKNA8x2M4iPbjAThQgVsAA545TWg8RQlc5MMBflrg8wHgg3NsIX6OAa10Lr6SDQgHgAXetSeJD5iqRTrytfAKdHwgiAu0EZ3QvEBhYLLAu6+LDoIOtCApKSnVUqoBDondyiahoTggazt9ej4hd94Wxbya4z5wskZoYVgA0/NXlGSlVr3iJpOpOMsbt36r4BphsACQHSezBfSP4zQQs8Zs3n7P3QmeDQ8OAwVwdkEt0ADhhI3jFCxms7lfu+yBthHhwcFhoaCgnXZC4obqPYbZPABqgHVpNj/yak1QABdRQatriwcIiXy312alrBRFqff175jFb9lrZecIVGgz09+NTurjbw/LNFmtUqnVw8PRM0AY6JowaHFN7iIKz2rvtCIjfbBPkvF992aapABKakzvKByQymxbO5e7iDb8BfjoA5uWsPv7kJxPSi4Oy7RpgA/+kl8KArdolUq1uDMshLXCA8jD6NXqpEQPAmDCszibTSFlccS4+DMGMCrVtvdzO0eEh59CPulfgsDYoQ/Rb4q1mbiCsX8zZ2AVrQLIV16uGRGMTxD6PIEkh2TBOPA4og09nSeYJoe+fND1SxdmSRCBgg+6PypE9o01mTRq6Bs+1yEQqcCHJK69cn62xDnA3YS6W4qD51kBA9LV6MX5aXKuIB90/wyJnztCQFMJKuif2zQANfANn9CnchlYzyWYF7OFV8ATgAPV624BAzQKCgTSSxIsDxIYOQR00m7kFgjpsQP6CgUlNWxFd7LDAEaOoPvoc74ESa1NsUBXK9QelLHfafTqclZmKwXvsjpJihyi/mkc9j3Su/P7BngxeMJHPXoru9vbCP4vYZ9aT/DMpxkAW7CsCCFZ3D5I5BowAPnp1wgnqRaGQ84kduEKemchwC5BJ1jX6Tt6Ey50SvBiMH1AgCvYA1i9BP8UkRtjsW8c3E5wjcstfCBxUQgW/APt9sBAfxIR/2ScBvnS5O6EgA4baL7w+hzpRELyhNSqZ1Jgf/0xQsgSuXPCQzxBSPwaG/YpawyRhRnrvBh0DvSGLkITn2A9M/Kljt5HiawsTGAwxVbohS7+JGPfOBgNEJDK3wg60d2EBTvM2KdWE27opPLCWPqEZP9W+BCLfIoa3JFwxyoLX4haVNFHSMUecQrsGx8RbmmZxgfoIV9KIeoDhg4d+jXTzPubCREWWmhOB0SV5hgxYkRGxpgxY36i+2cEgRixQLN1FpqnYJWkKklJScNLjxy5MyPjhwf0Kegn9yZEmZdAuxQqVwEkDR8+auTO77uhD3XKsbWOeKBZL4ugULlyuXJlyjQaNeqbFfhG6BuSwdeqOF0H0q6FBmyhUaNdu6FPQd/RHw4Qpxfj4heMatCwIZywh1JTLAYD+mkhSoeBLoGoqCENYGDXL943oB834iyhLdjHgfF79mHfsb0bkRvLBhZDjB49euzYsePH/96/vzyHeXN3Inc6zSuCqcRys1tJjm4xdYj//JP8AXE5S/JuAn7MAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=stackoverflow\.com]{padding-left:22px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAMAAABq2/00AAAA4VBMVEUAAACCg4aCg4aCg4bIj0P1fx/2hh+ojHCojHCojHD2ih/UjCn0eiD0eiD2hB/2hR/RjjDUjCnKkkPUjCnUjCn2ih/UjCnCllPCllP2ih/CllPCllPCllP2ih/CllPCllPCllP2ih/2ih/CllPCllP2ih/2ih/CllPUjCnCllPUjCn0eiD0eiD2ih+ojHD0eiDUjCn0eiD0eiCojHDUjCn0eiDUjCn0eiDUjCnUjCn0eiCojHD0eiCojHD2ih+ojHD2ih+ojHCojHCdiXaViHuCg4aojHD0eiDCllPUjCn2ih8XYwy7AAAARXRSTlMAv0CAEEAQv4BA7++/gCBQQN8wIIDPv2Dfv5+PUDDPryCAcO+/r49wn4Bg79/frJ9QcGDPr6+Pj3DPz2AwIJ9wYOaPVDAbIL/gAAAGhElEQVR42uzbsYrCQBDG8WmyxaJJlcIixQmxMSIRgiBB7Bbm/R/oPAnH3e0SxwSvyf/3DLt8zDeMAAAAAAAAAAAAAAAAAAAAAAAAAAAA4F9k7Xm1Xwlek53WqyY87AVWfbHumvCTE5hkIZYJTFyIrQU2+xA5CmxWIULcWq1DTGBzCrFcMDluC4ENcTtDEyKdwKYLkYuAuH27ggFtuj7ETgLiNo0B7Z3c1YnNMUQaWbTC6/W1uKUPHbha7wr60GnP7ot3YpGHWCsL5Wod1JP70LMsU+H12424nfDsBj4XgzMD2kN20F82YtDSh965nf61JW7Nzy7iKwY067OLlfJcw/qx1KQPQ9zSh1aaVtGHGnxoUummrB97WZhSk3byRM/6UST3mpQRtwZbTTo4GXdhQLvbaNJVxnX0oSMftyBuDQpN8o4BzaDWpFrG5KwfH5zXpJuMCfShox83pw812GnSxrx+XPI5hjvowF7ttawfB5km+Yq4nf5xS84x5lR7nGPMqfaI2znVHgPajGqPc4w51R7nGJ/sm29P2zAQh49h1kJgJHGWlrbQItAIebNO67owadpgwOzv/4Wm8S8NLsXxGSTu/HyEk/O78+McRu0FH4pQe2EdA6P2wjoGRu2FdotQe2EdA6P2wjoGRu2FdotQe+GChlF7DNcxKulN7bFbx4h1lPlSe+x8aKK1Tqae1B6zdYxM35BLP2qP1TqGjPQdmfCi9ji125l+ICp9qD1G6xixXiSPPag9PusYiW5SCLza49JuK/2YKEOrPSYXNBFpkyTGqj0e6xiFXkoukWqPwzpGrJ8iEyi1x8GHnuknSUqU2qO/jlHpVeQpwhB8oL6OISK9mkK4qz3q7bbQzxFV7mqP9jpGqi1IYme1R3sdI060BTPpaAiot9ss0hZkwk3tUb+gyUJbkEzd1B55HxqfaQvy1Ent0V/HKCNtwVy0NwSfGPhQMdcWRJWD2uOwjiFzbcFZ3F7tsVjHmDqMLTZqj3S7bTm2RJloZwh2maxjyJnd2NJS7XFZx7AcW2QrtUe93dZUDmOLjdrbaTTa7e9fNgj1i7ZjS9lK7dU+dPvzzgaRZ8elpPZji73a2/j749vOLuWytRtbCmmv9jghLMcWuw936zfwwnZssTAEexw+VidTmstn1N4Wr2/2gcrBlO6HY3eHKNqPLYfh2FmPLeZ/aXvh2D1Q2o0tYkHt8ZxQliIybTm2NNXex30IWI8tcePDPYSAywPv+61w7NwfeHfJHrvJS44tSQmUGan+oCfAgTi3fOCly1D952A4eqkH3gTocqDuOT5NX2JsmQJZhFrkaHwy8fzAmwNdeuoRRgQiTakEugyUSR2B+LFlDoTpK4M6AvH/pUUC6JIqAyMCMQ+8pMe8E2XgGIFlxKxbAIyVgUUEWj/wUh6QAZQBIgJlzqlbQKwMUBE4Tdh0C4ChMsBFoMgiJt2ivpvVoCNQznh0CxDKAB2B9dgSA2l6ygAbgfUDbwG0GSgDPxEo5tS7xYq7GT4CU8Im6pbRsNkxMBFIex5ezqQ3qM8fPgL5kZ6Mj1QNLgKpR90y4tNjpbxFID+ExwjsAUN8ReAYmOIjAlmePF8RyLHv+orAPgScI3AIb531tQZ/XjECR3DL5VoTeDOsXTVYf8UIFHDLu6sm8GZAFA8ZgccQimcgLCPwNBRvOROLCExD8Z4mXR2BRxCK5+wCx6F47hHYC8Vzj8BJKJ5zBPYhFM85Agf0i3dx3ulcdLubm5e+I3BEv3id65r7Ov70EoGCV/EW+NrpdLu/cHWkX7zz65pn6hiK9689O8hJKIaiMCxK0nYzTBgSYxDs/pekiTohEq/v0eRKv28J/+zkXMbrF2IdxQvEC3ScOF7r6+3LrtZDa1vxVinfHaeId+qjTBCv9kGO4i1XJoi365/EWxCv9EF24i1XJ4jXv4iXKd7p/uNt+yjt/uO1PspZvOVmmGcPrZ1q3ZUi3qoDaNva4WYdj5PFu2nHMm+8nzqKF4y3vuOreL923PcrqngRz6291FrKXrxbdWzifXB6iydehHh5iDco3maQpxnivQ2yEU888cQLEi8R8cSLES8R8cSLES8R8cSLES8R8cSLES8R8QbFexzkPEO86xxA4on3F+LlIZ54QeLlIZ54QeLlIZ54QeLlId6qeOk8/BviiRciXibiiRciXibiiRciXibiiRciXibiiQcAAAAAAAAAAAAAAAAAwHTeAe21evvWi2VXAAAAAElFTkSuQmCC")}google-codelab-step .instructions .faq a[href*=support\.google\.com\/webmasters\/]{padding-left:24px;-o-background-size:24px;background-size:24px;background-repeat:no-repeat;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAolBMVEUAAADW1tbW1tZ6enrQ0dJ6enrMzMzq6+zq6+x6enru7+/m5+ju7+/m5+j////S09REi/XW1tbm5+ju7+9PT0/Q0dK8vLxGjfVZWVlNTU16enrMzMzIyMj09PS/0uuSkpJpaWmbm5tim/e0tLSEhIStx+3e3+GsrKxwcHBZk/dPk/R1p/KPtu/U3eqmpqZlZWVfmO5woeqHreWvwN6nu9qLi4vZSE73AAAADnRSTlMAEcxmzO7MzDMz7u6IiHn/rpYAAALMSURBVHja7dXpjtowFIZhUkqZlSV1ncQdEsK+zD7T+7+1xjTiwxVOAmeORyP5+2UspPchEtDy8/Pz8/Pz8/M7cZeduHKdyxbL0FfVAMUraHdUHUB12i22tZWqBRSDgKFfC4CApV8PgIClXw/gE6hyTd/lAR7gAV8fEHwbWtc/ul/WfQ8IfQKAIECfCoCA3sdq+nQB+nQABIQ+CQABtY9Z+lQB+gwACBj6AFAE6DM9AL0fAalPB/yGgNAfntwHAAJKnwCAwNLneQAAVAvM/ttzVLlB4z2/A6AFVoDRf43IfewVgGKNAG/Uvrl39BsCXqh9cy8nA/51tmk63+jDZp6m9wTA4DzANiyW6lOqTxAMHAD22XASReuwpKDvELCNookBGJwx9E8B3OvsQp8e9GmOviNANA/Dh4k+TApBukHfFSBa7/I7whqf3xWA/gOAoU8DDJwBCHk+wIAw9CsAPw/W++CFB/MAD/hCgL59PXPS8toRYKHEndkTauEQIIUQ0hToG2eAXOicKdBXmSPAo9CTpkDo5U4Ad6IEGILy0gFgFJctCACIh/yATABwIChvVyE3IBR7AAQAiOmIGZADAMEBQMkRL+AJAAgOALGUI07AQgBgCPb3uRbwAaYAGAIAZlIL2AA5AIYAgJXUAjaAAsAQADCWO4EjAAQGQAvYAUsDsDQAeuwAMZvu89PZ/jZhBmQCi2dl60+MPiMAf8UgZEUpK/LoJ4oX0C9LIIyF0U8yXkBPVCzRWzIDZjX9sWQG9Kv7ScYN6KnKfizZAf2qfpLzA3p5RV9JboDeytp/kk4Ao5WtP+UFQKCO9sdTSQQ03+OR/kxKdgAWZv/11VI6AWBmP5HSNQD5zwQknwxIqIBu468hBgDqNYCuFXBDASSNAbdWQHB1PiBpDLgOWtZdXPEDri/QswjYAOjbF9x0OQHd26Dl5+fn5+fn5+dn7i/3LEaKJNV/0wAAAABJRU5ErkJggg==")}google-codelab-step .instructions .faq a[href*=android-developer],google-codelab-step .instructions .faq a[href*=developer\.android\.com]{padding-left:20px;background-repeat:no-repeat;-o-background-size:20px;background-size:20px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAq1BMVEX///+lyjmfxyGhyCqkyjakyTP+/vuiyS7z+OWlyjKgxyTX57ChyCfa6bTx9uLy9+Sz0l6szk/D24Tt9NrN4ZnK4JSiyCyexh34+/D6/PTp8dPh7cjf7L7k78vc6rnW5qzU5ajS5KPP4p+21GWnzD3F3Yi813Kx0ViqzUWexhr8/vjv9d7Z6LKy0lyZxADh7cLA2n2rzkr1+erJ35C61m6uz1KcxRHn8M6/2XgEePWtAAACr0lEQVRo3u3Z2XKiQBiG4Y9ebNO2aCCyzOC+7zH7/V/ZRJZQZaJpWnKS4TlJUItXbX4pFJXK71DTusvccI6z5oMSAutdhDP2uzpK4CxwxsRBGYJdA19qLAOU4s4LkyUN7G4Ude2glmzuRihHKDqAPZhNuJRKSckms37rvctrKMnG728l5cxKMU7JuCF7KEvf87h1gnu0iXK4B2p9Sc3dUgZNMusMLvu42r20LiBXT8JMWRepN1xlGu+fK8/6xFPxwsvH697/eP/j7uDTOvB+dxwX/AaM2SQ+6icrYHDyGmgfWE3irNeCqTaPAwcArydroboAbuMAb8NQRNJn20SYtHK8HaKZzoffhZl5tlNxsD5PsnUQ2b9jwwnO35WvRo3lN/o2TDwKS5M3gokF0w2wZxgIlpY2FpgsgdQPkLXJQar0A/LVIDB6Idp2GxQQrJI/rbq2Voijlc5S9LZs8uYCqBVyXLTZhLW/fSn3hDMmZAQ05I020gT2PmeM+w4uGpLksFs+oEEtbaqJVnpUkz4uyebf6xQK0CY6XjoSC1xQ9/PPr4KB52zuvTrOa5EssC0auM0C0tYKtI0DqgpUgSpQBapAFfifAj90Tn5YZoGnooH8eutB56KDDosGhlTrYiTyk0epVdFAKJLn9rLHRSNfcE4tF0UDcAXlXPhDfGPtjGejEMUDCEdPY8eFHoOADvPAxiDQLBBQkUHAJfoB34UBS/9CfAETe6lZYKZft/QIFe94vicRy7tcvKOkB0PhYOo4zlNWYNvjpjOdZwU+O24OQlynK62EGCF2J9IbpIsy/P0I3CHW+Qj8qQJVoAr8VOCx3EB+YvDSwIDmXzaW4oZZseU6Lfr5aaAUPT8uyPuT33VeIpRkI5RHyRQfHEKFUj2UJuh1GjaQsxudKECl8jv8A6GtQkKSkMLrAAAAAElFTkSuQmCC")}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{color:#000;text-decoration:none;padding-left:24px;background-repeat:no-repeat;-o-background-size:18px;background-size:18px;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAMAAAAOusbgAAAAflBMVEUAAACXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZaXlZbf2s+YAAAAKXRSTlMA+SAC7QrIBPXROWMX17rCSmusXEaAD9zNeXFWJqOLMx3mtJJAK5xP4Jw4LyAAAAT6SURBVGje1NbdcqJAEAXg0zJAQEFA0QhojJHE8/4vuBfrJlvQGHSGbO13CzVT09M/g//HIssPRZAYIUkxSVAc8myBSfnrMjVUmbRc+5jEsUqFN0lazeDYogo4SlC5jPq6EI4mxRpOhKuId4pWof228y0fsJ1bbp0lfFCS4XFNSgtpg8eEO6EV2YV4QBvRWtTiXl4udEByD3fxCzpS+LhDk9CZpMFosaFDJsZItdApqTHKnM7NMULOCeQW5534zDUnUuOmWDgRiXFDYzgZ02CQn3BCiY8BXsFJFd7YQjLPkeGDTPRsRhZVK+zIABxX+yfe6Wm/OgL4YIe0UIQRO8x1jm/yiHeI8s11RdP7FKJvx649PmUBRwoyfNqza6dUktzsNt7HliNsa+9m95UGXSl7XvQn2OV0qOpzO1tsNotZe66rw+lyXbb08bcX9qToyNgjYTcoS3PaxT4UflydzLJ7nFDYk3V+SdhzQZfnQTP89VW5jPDbmRTAWvDdnAq1zFnC2pLfHHnFn9uYK3yJ+HOhZoRPa2peYe2VmjX+KKjawNKGqgJXC6HqDEtnqmSB3yrqSlgqqau+UkAVwVLEKz1tZxxQw1LNATObSNvHOqUqCGEtDKhKAcAXqlo40Ag14g92D77BiTcO9pByqNbsDfeIcvCK3+HI++AlG2piOBJTY4AjNVs4s6XmiMwitSzSK0Nu0bQs2leOAzUzODOj5qDPYvHgTCj6TA703HLoog+oxGIgWgzHBMbigWnx1DSQyTcOqBDw34Sav9q1sx1HYSAKoNcYE4cl0AlhIB22rF3//4PTI43ULbrMYjzKy5zHROiGyNhlyqAXDC5z8Acc6mcFu3+OpeCD+Y87ONMRR1isik6qet+wah3gzMEwfIPXLIsB9sQp4UxJnL2pDFRwRBHrauoEZHDkaeoV5PSKKpNydMTSCk4oTawO8O07Nva9JN+8Z0skHNgk5l3bkf7hLUcjW+CceH2B1YqeeDmzTXU5sB/EE4rZmDucsA9ksBtvJuoQq4SaDE5sqc8n2+eaNyqBOTmHtcycG8xo3B4lrMgjmZ0GrwtYaQsLbUBmImZeojKuHRbyHjRmz5ZFQRUdByfGxCPEfPJW07gbVyVoiU9xIwYz97GVs1LD5oMmbPknvcKntLmXNNDvT2GBEV1WXXyadjA0Q64bIMygLsTo00aCsbmWfCbfDOEnt6QF8EzjlBi6BasVdscU5HYwbdz9LO65y9ae4tjKwTzDTJUZfxlPJjRLhoELfdPHgNrl9ZIFK6I5LhjyxOB75QdFsqAEVIKmCW+qcZ0B9xvOPft7eReaVnFPRPmj1lPvbcJdx6toUrkB46zpmyeAnFLViMGDZvSkKfo8Y3ikAORjB8RVqonIT4+hxJjW/qRRzezNvd1VoegKoG1uGOPRhBomaktfAok/PJ9a3PykQEk1xsQ0bqtg5Pk/+04qBtryotDlG4wpaJTvzS7Q3sCwDdbhgu1sw9yi7V/9XFSEb3MwbAbXYWn5H7zF3+5J2gZHNhuPX5frsaqaOvWpsAw+rC3FY6tg/cRMYU88zya4DzGbV7oL3npYQNWugmuFZSLtIlhHWOwcrA8OzrAgT3pdsD5J2PH2a4L3HuzdStvgMscq8j2hL/e5wcm7BLA2OuBbFebVKWBirYR/D6OWc16JizqEO0WUCrHzMMrbCZFGBf5j/QYa/td1VlNoIAAAAABJRU5ErkJggg==")}google-codelab-step .embedded-iframe,google-codelab-step .youtube-video{display:-webkit-box;display:-webkit-flex;display:flex;margin:auto;width:560px;height:315px;border:none;max-width:100%;max-height:51vw}google-codelab google-codelab-step .note:before,google-codelab google-codelab-step .special:before,google-codelab google-codelab-step aside:before{content:unset!important}google-codelab google-codelab-step .instructions aside.special,google-codelab google-codelab-step .instructions aside.warning{margin:10px 0!important;padding:15px 20px!important}iron-icon{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;font-size:inherit}iron-icon[icon=file-download]:after{content:"cloud_download"}google-codelab .warning,google-codelab .warning :link,google-codelab .warning :visited,google-codelab .warning code{background:#fef7e0}google-codelab .special :link,google-codelab .special :visited,google-codelab .special code,google-codelab aside :link,google-codelab aside :visited,google-codelab aside code{background:#e6f4ea}google-codelab-step td{background:transparent}google-codelab-step .instructions h3>a[href*=github],google-codelab-step .instructions h3>a[href*=github]:visited{background-position:0 3px}google-codelab-step code .pln,google-codelab-step code .pun,google-codelab-step pre .pln,google-codelab-step pre .pun{color:inherit}google-codelab-step code{font:500 90%/1 Roboto Mono,monospace}google-codelab-step pre{background:#f1f3f4;-webkit-border-radius:0;border-radius:0;color:inherit;margin:16px 0;overflow-x:auto;padding:8px 80px 8px 8px;position:relative}google-codelab-step pre,google-codelab-step pre code{font:14px/20px Roboto Mono,monospace}.button-blue,.button-green,.button-primary,.button-red,body devsite-footer-utility .devsite-footer-utility-button>a{color:#fff!important}google-codelab{width:100%;height:100%;padding-top:64px}google-codelab,google-codelab #main{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #main{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;position:relative;background:#f8f9fa}google-codelab #codelab-title{position:fixed;top:0;left:0;width:100%;background:#fff;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);color:#3c4043;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;height:64px;padding:0 36px 0 16px;-webkit-font-smoothing:antialiased;z-index:1000}google-codelab #codelab-title h1{font-size:20px;font-weight:400;margin:0 8px;font-family:Roboto,Noto,sans-serif;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-shrink:1;flex-shrink:1;white-space:nowrap;-o-text-overflow:ellipsis;text-overflow:ellipsis;overflow:hidden;width:0;display:inline-block}google-codelab #codelab-title .time-remaining{-webkit-flex-shrink:0;flex-shrink:0;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:16px;font-weight:400;white-space:nowrap}google-codelab #codelab-title .time-remaining i{margin-right:3px}google-codelab #codelab-nav-buttons{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-flex:0;-webkit-flex-grow:0;flex-grow:0;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu{text-decoration:none;color:#3c4043;width:40px;height:40px;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #codelab-nav-buttons #arrow-back,google-codelab #codelab-nav-buttons #menu,google-codelab #controls{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}google-codelab #controls{position:absolute;bottom:32px;left:0;right:0;padding:0 32px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;z-index:1001}google-codelab #fabs{display:-webkit-box;display:-webkit-flex;display:flex;max-width:1025px;width:100%;margin:0 auto}google-codelab #fabs,google-codelab #fabs .spacer{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}#done,#next-step,#previous-step{-webkit-border-radius:4px;border-radius:4px;font-family:Google Sans,Arial,sans-serif;font-size:14px;font-weight:600;letter-spacing:.6px;line-height:24px;padding:6px 24px;pointer-events:auto;text-transform:none;background:#fff;color:#1a73e8;-webkit-transform:scale(1);-o-transform:scale(1);transform:scale(1);-webkit-transition:-webkit-transform .3s ease-in-out;transition:-webkit-transform .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out;transition:transform .3s ease-in-out;transition:transform .3s ease-in-out,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out;-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);text-decoration:none;-webkit-font-smoothing:antialiased}#next-step{color:#fff;background:#1a73e8}#done{background:#1e8e3e;color:#fff}google-codelab #fabs a[disappear]{-webkit-transform:scale(0);-o-transform:scale(0);transform:scale(0)}#done{background:#0f9d58}google-codelab #drawer .codelab-time-container{display:none}@media (max-width:768px){google-codelab #codelab-title .codelab-time-container{display:none}google-codelab #drawer .codelab-time-container{display:block;padding:20px 10px 10px 23px}google-codelab #drawer .time-remaining i{margin-right:9px}}google-codelab #drawer{background:#fff;width:256px;-webkit-flex-shrink:0;flex-shrink:0;position:relative;z-index:100;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;background:#f8f9fa}google-codelab #drawer,google-codelab #drawer .steps{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer .steps{-webkit-flex-shrink:1;flex-shrink:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;overflow-x:visible;max-height:-webkit-calc(100% - 54px);max-height:calc(100% - 54px)}google-codelab #drawer .steps:only-child{max-height:100%}google-codelab #drawer .metadata .material-icons{top:6px;position:relative}google-codelab #drawer ol{margin:0;padding:16px 12px;counter-reset:li-count;list-style:none;overflow-x:visible;overflow-y:auto;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab #drawer ol li{display:block;counter-increment:li-count}google-codelab #drawer ol li a{text-decoration:none;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;font-size:14px;color:#80868b;padding:3px 10px;min-height:48px;font-weight:400;line-height:20px;-webkit-box-sizing:content-box;box-sizing:content-box;position:relative;font-family:Roboto,Noto,sans-serif;-webkit-font-smoothing:antialiased;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out;border:1px solid #dadce0;-webkit-border-radius:5px;border-radius:5px;margin:6px 0;background:#fff}google-codelab #drawer ol li a:active,google-codelab #drawer ol li a:focus{background:#c6c6c6;-webkit-tap-highlight-color:transparent;outline:0;border-color:#c6c6c6!important}google-codelab #drawer ol li a .step{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center}google-codelab #drawer ol li .step:before{content:counter(li-count);display:inline-block;font-style:normal;width:26px;min-width:26px;color:#fff;background:#80868b;-webkit-border-radius:50%;border-radius:50%;text-align:center;height:26px;vertical-align:middle;line-height:26px;margin-right:8px;font-weight:400;position:relative;z-index:2;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}google-codelab #drawer ol li[selected] a,google-codelab #drawer ol li a:focus{color:#212121;font-weight:600;-webkit-box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15);box-shadow:0 1px 2px 0 rgba(60,64,67,.3),0 2px 6px 2px rgba(60,64,67,.15)}google-codelab #drawer ol li[selected] a{border-color:#fff}google-codelab #drawer ol li[selected] .step:before{font-weight:600}google-codelab #drawer ol li[completed] a{color:#212121}google-codelab #drawer ol li[completed] .step:before{background-color:#1a73e8;color:#fff}google-codelab #drawer .metadata{color:#777;font-size:14px;padding:16px;-webkit-flex-shrink:0;flex-shrink:0}google-codelab #drawer .metadata a{color:currentcolor;margin-left:4px}google-codelab #codelab-nav-buttons #menu{display:none}google-codelab #drawer ol ::-webkit-scrollbar{-webkit-appearance:none;width:7px}google-codelab #drawer ol ::-webkit-scrollbar-thumb{-webkit-border-radius:4px;border-radius:4px;background-color:rgba(0,0,0,.5);-webkit-box-shadow:0 0 1px hsla(0,0%,100%,.5)}@media (max-width:768px){google-codelab{display:block;position:relative}google-codelab #main{height:100%}google-codelab #codelab-nav-buttons #arrow-back{display:none}google-codelab #codelab-nav-buttons #menu{display:-webkit-box;display:-webkit-flex;display:flex}google-codelab #drawer{width:256px;position:absolute;left:0;top:0;bottom:0;z-index:10000;will-change:transform;-webkit-box-shadow:2px 2px 4px transparent;box-shadow:2px 2px 4px transparent;pointer-events:none;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);-webkit-transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;transition:-webkit-transform .3s ease-in-out,-webkit-box-shadow .3s;-o-transition:box-shadow .3s,-o-transform ease-in-out .3s;transition:transform .3s ease-in-out,box-shadow .3s;transition:transform .3s ease-in-out,box-shadow .3s,-webkit-transform .3s ease-in-out,-o-transform .3s ease-in-out,-webkit-box-shadow .3s}google-codelab[drawer--open] #drawer{-webkit-box-shadow:2px 2px 4px rgba(0,0,0,.15);box-shadow:2px 2px 4px rgba(0,0,0,.15);-webkit-transform:translateZ(0);transform:translateZ(0);pointer-events:all}google-codelab #main:before{content:"";top:0;left:0;right:0;bottom:0;position:absolute;-webkit-transition:opacity .38s ease-in-out;-o-transition:opacity ease-in-out .38s;transition:opacity .38s ease-in-out;background-color:rgba(0,0,0,.3);z-index:10;pointer-events:none;opacity:0}google-codelab[drawer--open] #main:before{opacity:1;pointer-events:all}}google-codelab #steps{overflow:hidden;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;position:relative;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}google-codelab google-codelab-step{display:none;width:100%;-webkit-transform:translateZ(0);transform:translateZ(0);position:absolute;top:0;left:0;right:0;bottom:0;padding-top:32px;overflow-y:auto;overflow-x:hidden}google-codelab google-codelab-step[animating],google-codelab google-codelab-step[selected]{display:block;-webkit-transform-origin:0 50% 0;-o-transform-origin:0 50% 0;transform-origin:0 50% 0;-webkit-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both}google-codelab google-codelab-step[animating]{pointer-events:none;position:absolute;overflow:hidden}google-codelab #drawer ol li{padding:0;margin:0}google-codelab{height:100vh;left:0;position:fixed;top:0}google-codelab #codelab-title h1{width:auto;color:#3c4043;top:0}google-codelab .title{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;margin-left:10px;overflow-x:hidden}google-codelab #drawer .metadata .material-icons,google-codelab-about .about-card .material-icons{top:0!important}body[type=codelab]{color:#5c5c5c;font-family:Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;overflow:hidden}body[type=codelab] google-codelab{opacity:0;display:-webkit-box;display:-webkit-flex;display:flex}body[type=codelab] devsite-googler-buttons{bottom:95px}body[type=codelab][ready] google-codelab{opacity:1}body[type=codelab] .devsite-main-content{max-width:100%!important;padding:0!important}body[type=codelab] a:focus{text-decoration:none}body[type=codelab] .devsite-badger-award{left:256px}body[type=codelab] .devsite-back-to-top-link,body[type=codelab] .devsite-banner,body[type=codelab] .devsite-footer,body[type=codelab] .devsite-heading-link,body[type=codelab] devsite-code:after,body[type=codelab] devsite-header{display:none}@media screen and (max-width:840px){body[type=codelab] .devsite-badger-award{left:0}}body.google-samples{background:#f1f3f4}body.google-samples .devsite-article{margin:0;width:auto}android-samples{display:block;--mdc-theme-primary:$WHITE}android-samples .mdc-button:not(:disabled){color:#fff}android-samples .sample-grid{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-flex-wrap:wrap;flex-wrap:wrap;margin:-24px 0 16px -24px}android-samples .resource-card{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 33.3333%;flex:0 0 33.3333%;min-width:0;padding:24px 0 0 24px}android-samples .resource-card:active,android-samples .resource-card:focus{text-decoration:none}android-samples .sample-card{background:#fff;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.21);box-shadow:0 1px 3px 0 rgba(0,0,0,.21);-webkit-box-flex:1;-webkit-flex:1 0;flex:1 0;max-height:165px;min-width:0;overflow:hidden;padding:10px 20px 20px;position:relative}android-samples .card-featured .card-info:before{color:#1a73e8;content:"FEATURED";font-size:10px;position:absolute;right:10px;text-align:right;top:8px}android-samples .card-info .section{color:#9aa0a6;font:700 11px/20px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;letter-spacing:.3px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}android-samples .card-info .title{color:#3c4043;font:500 18px/23px Roboto,Noto Sans,Noto Sans JP,Noto Sans KR,Noto Naskh Arabic,Noto Sans Thai,Noto Sans Hebrew,Noto Sans Bengali,sans-serif;margin:7px 0;max-height:46px;overflow:hidden;-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:normal}android-samples .sample-description-area .text{color:#5f6368;font-size:14px;line-height:20px;margin:0 0 100px;max-height:60px;overflow:hidden}android-samples .no-display,android-samples .sample-description-area :not(.text){display:none}android-samples .block-display{display:-webkit-box;display:-webkit-flex;display:flex}@media screen and (min-width:1440px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 25%;flex:0 0 25%}}@media screen and (max-width:840px){android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 50%;flex:0 0 50%}}@media screen and (max-width:600px){android-samples .sample-grid{margin:-16px 0 8px -16px}android-samples .resource-card{-webkit-box-flex:0;-webkit-flex:0 0 100%;flex:0 0 100%;padding:16px 0 0 16px}}android-samples .navrow{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:#546e7a;display:-webkit-box;display:-webkit-flex;display:flex;max-height:68px;padding:16px;visibility:hidden}@media screen and (max-width:840px){android-samples .navrow{max-height:112px}}@media screen and (max-width:600px){android-samples .navrow{max-height:184px}}body[mdc--ready] android-samples .navrow{max-height:none;visibility:visible}android-samples .mdc-menu-surface[open]{display:block;opacity:1}android-samples .navrow .buttongroup{display:-webkit-box;display:-webkit-flex;display:flex}android-samples .sample-menu{padding:0 4px 0 16px}android-samples .sample-button,android-samples .sample-menu{-webkit-box-align:center;-webkit-align-items:center;align-items:center;background:0;-webkit-box-shadow:none;box-shadow:none;color:#fff;display:-webkit-inline-box;display:-webkit-inline-flex;display:inline-flex;opacity:.87}android-samples .sample-button:hover,android-samples .sample-menu:hover{background-color:hsla(0,0%,62%,.2)}android-samples .sample-button:active,android-samples .sample-button:focus,android-samples .sample-menu:active,android-samples .sample-menu:focus{background-color:hsla(0,0%,62%,.4);-webkit-box-shadow:none;box-shadow:none}android-samples .mdc-button{letter-spacing:0}android-samples .mdc-list-item__text{font-size:14px}android-samples .googlesamples-filter-menu{padding-left:0}android-samples .menu-indicator{font-size:20px;visibility:hidden}android-samples .menu-indicator.yes-visible{visibility:visible}android-samples .navrow .searchfield{font-size:16px;margin:0 0 0 auto;width:300px}android-samples .navrow input#search{background:#fff;border:0;-webkit-border-radius:2px;border-radius:2px;margin:0;padding:4px 0 4px 8px;width:100%}android-samples .navrow .searchfield label{display:none}@media screen and (max-width:840px){android-samples .navrow{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:16px 16px 24px}android-samples .navrow .searchfield{margin:8px 0 0}}@media screen and (max-width:600px){android-samples .navrow{margin:0 -16px}android-samples .navrow .buttongroup{-webkit-align-self:flex-start;align-self:flex-start;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}android-samples .navrow .searchfield{margin:8px 16px 0;width:100%}}android-samples .controls-grid{-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;padding:16px 24px}android-samples .matching-samples-count,android-samples .per-page-selector{-webkit-box-flex:1;-webkit-flex:1 1 20%;flex:1 1 20%;white-space:nowrap}android-samples .page-number-selector{-webkit-box-flex:1;-webkit-flex:1 1 60%;flex:1 1 60%;text-align:center}android-samples .matching-samples-count{text-align:right}android-samples .page-number-selector-button,android-samples .per-page-item{background:0;border:0;-webkit-border-radius:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none;color:#1a73e8;cursor:pointer;font-size:16px;font-weight:400;height:auto;margin:0;min-width:auto;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .page-number-selector-button:hover,android-samples .per-page-item:active,android-samples .per-page-item:focus,android-samples .per-page-item:hover{background:0}android-samples .page-number-selector-button:hover,android-samples .per-page-item:hover{color:#000}android-samples .page-number-selector-button:active,android-samples .page-number-selector-button:focus,android-samples .per-page-item:active,android-samples .per-page-item:focus{-webkit-box-shadow:none;box-shadow:none}android-samples .page-number-selector-button .material-icons{margin-bottom:4px;vertical-align:middle}android-samples .page-number-selector-inactive,android-samples .per-page-selected-item{color:#000;cursor:default}@media screen and (max-width:840px){android-samples .controls-grid{display:block;padding:16px 0;text-align:center}android-samples .matching-samples-count,android-samples .per-page-selector{margin:8px auto;text-align:center}}@font-face{font-family:Euclid;src:url(../fonts/custom/euclid.ttf) format("truetype"),url(../fonts/custom/euclid.otf) format("opentype"),url(../fonts/custom/euclid.woff) format("woff")}android-sticky-toc ul,android-sticky-toc ul ul{list-style:none;padding-left:0}android-sticky-toc a:focus,android-sticky-toc a:hover,android-sticky-toc a:link,android-sticky-toc a:visited{color:#414141}android-sticky-toc[sticky]{padding-left:32px;position:-webkit-sticky;position:sticky;top:164px;width:240px}android-sticky-toc[embedded]{display:none;margin-top:120px}android-sticky-toc .dac-nav-item.dac-nav-heading{line-height:16px;margin-bottom:16px;max-width:160px}android-sticky-toc .dac-nav-subtitle{font:500 12px/22px Roboto Mono,monospace;color:#414141;text-transform:uppercase}android-sticky-toc .dac-nav-title{font-size:13px}android-sticky-toc .dac-nav-item{margin:0;padding:5px 0}android-sticky-toc div>ul{border-left:4px solid #f5f5f5;padding-left:24px}android-sticky-toc div>ul .dac-nav-title{border-left:4px solid transparent;display:inline-block;margin-left:-28px}android-sticky-toc ul ul .dac-nav-title{padding-left:24px}android-sticky-toc div>ul>.dac-nav-item:first-child{padding-top:0}android-sticky-toc div>ul>.dac-nav-item:last-child,android-sticky-toc div>ul>.dac-nav-item:last-child .dac-nav-item:last-child{padding-bottom:0}android-sticky-toc .dac-nav-title.dac-nav-active{border-left:4px solid #3ddc84;font-weight:500}android-sticky-toc .dac-nav-title .dac-nav-arrow:before{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;content:"arrow_right";vertical-align:middle}android-sticky-toc .dac-nav-title .dac-nav-arrow.open:before{content:"arrow_drop_down"}android-sticky-toc .dac-nav-list.nested{height:0;visibility:hidden}android-sticky-toc .dac-nav-list.nested.collapse{height:auto;padding-top:5px;visibility:visible}@media screen and (max-width:1200px){android-sticky-toc[sticky]{display:none}android-sticky-toc[embedded]{display:block}}@media screen and (max-width:1000px){android-sticky-toc[embedded]{display:none}}.dac-tutorial-page[ready] .devsite-wrapper{overflow:inherit}.dac-tutorial-page .dac-preview-wrapper{height:-webkit-calc(100vh - 96px);height:calc(100vh - 96px);position:-webkit-sticky;position:sticky;top:64px;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview{opacity:0;position:absolute;visibility:hidden;width:100%}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.active{opacity:1;visibility:visible}.dac-tutorial-page .dac-preview-wrapper .dac-step-preview.screenshot{border:0;-webkit-transition:visibility 0s .3s,opacity .3s linear;-o-transition:visibility 0s .3s,opacity .3s linear;transition:visibility 0s .3s,opacity .3s linear}.dac-tutorial-page .dac-step-wrapper section.active:before{content:"";border-left:4px solid #3ddc84;-webkit-border-radius:4px;border-radius:4px;margin-left:-20px;padding-left:16px}.dac-tutorial-page .dac-preview-toggle-wrapper{color:#fff;position:absolute;right:0;top:27px;z-index:1}.dac-tutorial-page devsite-code .devsite-icon-copy:before{z-index:2}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:none}@media screen and (max-width:1000px){.dac-tutorial-page .dac-step-wrapper section.active:before{content:none}.dac-tutorial-page .dac-preview-toggle-wrapper,.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle{color:#414141;left:0;position:relative;right:0;top:0}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle+.dac-preview-phone{display:none;padding-top:40px;margin:auto;width:200px}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-toggle.toggle+.dac-preview-phone{display:block}.dac-tutorial-page .dac-preview-toggle-wrapper .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper .hide,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .show{display:none!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .dac-preview-phone,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide{display:block!important}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after,.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{font:normal normal normal 24px/1 Material Icons;-webkit-font-feature-settings:"liga";font-feature-settings:"liga";-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;text-transform:none;word-wrap:normal;height:23px;position:absolute;width:50px}.dac-tutorial-page .dac-preview-toggle-wrapper .show:after{content:"expand_more"}.dac-tutorial-page .dac-preview-toggle-wrapper.toggle .hide:after{content:"expand_less"}}
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 7f809f1..cbb96f5 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -252,6 +252,9 @@
docs(project(":privacysandbox:tools:tools-apicompiler"))
docs(project(":privacysandbox:tools:tools-apigenerator"))
docs(project(":privacysandbox:tools:tools-core"))
+ docs(project(":privacysandbox:ui:ui-client"))
+ docs(project(":privacysandbox:ui:ui-core"))
+ docs(project(":privacysandbox:ui:ui-provider"))
docs(project(":profileinstaller:profileinstaller"))
docs(project(":recommendation:recommendation"))
docs(project(":recyclerview:recyclerview"))
diff --git a/docs/onboarding.md b/docs/onboarding.md
index 7673b4e..56fd9f3 100644
--- a/docs/onboarding.md
+++ b/docs/onboarding.md
@@ -184,11 +184,11 @@
allows you to explore all of the source code in the repository. Links to this
URL may be shared on the public issue tracked and other external sites.
+### Custom search engine for `androidx-main` {#custom-search-engine}
+
We recommend setting up a custom search engine in Chrome as a faster (and
publicly-accessible) alternative to `cs/`.
-### Custom search engine for `androidx-main` {#custom-search-engine}
-
1. Open `chrome://settings/searchEngines`
1. Click the `Add` button
1. Enter a name for your search engine, ex. "AndroidX Code Search"
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 53a6ddb..9156e71 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -28,12 +28,22 @@
implementation project(path: ':emoji2:emoji2')
implementation project(path: ':core:core')
- androidTestImplementation(libs.testRunner)
androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation project(path: ':test:screenshot:screenshot')
}
android {
+ defaultConfig {
+ minSdkVersion 21
+ }
+ sourceSets {
+ androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/emoji2/emoji2-emojipicker"
+ }
+
namespace "androidx.emoji2.emojipicker"
+ testOptions.unitTests.includeAndroidResources = true
}
androidx {
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..45dad90
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:name="androidx.emoji2.emojipicker.EmojiViewTestActivity" />
+ </application>
+</manifest>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
new file mode 100644
index 0000000..dd4ff10
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiViewTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.app.Activity
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.ForegroundColorSpan
+import androidx.core.graphics.applyCanvas
+import androidx.core.text.toSpanned
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.test.screenshot.assertAgainstGolden
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+class EmojiViewTestActivity : Activity()
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class EmojiViewTest {
+ companion object {
+ private const val GRINNING_FACE = "\uD83D\uDE00"
+ }
+
+ @get:Rule
+ val screenshotRule = AndroidXScreenshotTestRule("emoji2/emoji2-emojipicker")
+
+ @get:Rule
+ val activityRule = ActivityScenarioRule(EmojiViewTestActivity::class.java)
+
+ private lateinit var emojiView: EmojiView
+
+ @Before
+ fun setUp() {
+ activityRule.scenario.onActivity {
+ emojiView = EmojiView(it)
+ it.setContentView(emojiView)
+ }
+ }
+
+ private fun setAndWait(cs: CharSequence?) {
+ emojiView.emoji = cs
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync()
+ }
+
+ private fun dumpAndAssertAgainstGolden(golden: String) {
+ Bitmap.createBitmap(128, 128, Bitmap.Config.ARGB_8888).applyCanvas {
+ emojiView.draw(this)
+ }.assertAgainstGolden(screenshotRule, golden)
+ }
+
+ @Test
+ fun testDrawEmoji() {
+ setAndWait(GRINNING_FACE)
+ dumpAndAssertAgainstGolden("draw_grinning_face")
+ }
+
+ @Test
+ fun testDrawSpannedString() {
+ setAndWait(SpannableString("0").apply {
+ setSpan(ForegroundColorSpan(Color.RED), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }.toSpanned())
+
+ dumpAndAssertAgainstGolden("draw_red_zero")
+ }
+
+ @Test
+ fun testMultipleDraw() {
+ setAndWait(GRINNING_FACE)
+ setAndWait("M")
+
+ dumpAndAssertAgainstGolden("multiple_draw")
+ }
+
+ @Test
+ fun testClear() {
+ setAndWait(GRINNING_FACE)
+ setAndWait(null)
+
+ dumpAndAssertAgainstGolden("draw_and_clear")
+ }
+}
diff --git a/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..280a186
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
new file mode 100644
index 0000000..abd602c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiView.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.os.Build
+import android.text.Layout
+import android.text.Spanned
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.util.TypedValue
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.core.graphics.applyCanvas
+
+/**
+ * A customized view to support drawing emojis asynchronously.
+ */
+internal class EmojiView(context: Context) : View(context) {
+ companion object {
+ private const val EMOJI_DRAW_TEXT_SIZE_SP = 30
+ }
+
+ private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {
+ textSize = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_SP,
+ EMOJI_DRAW_TEXT_SIZE_SP.toFloat(),
+ context.resources.displayMetrics
+ )
+ }
+
+ private val offscreenCanvasBitmap: Bitmap = with(textPaint.fontMetricsInt) {
+ val size = bottom - top
+ Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val size = MeasureSpec.getSize(widthMeasureSpec)
+ setMeasuredDimension(size, size)
+ }
+
+ override fun draw(canvas: Canvas?) {
+ super.draw(canvas)
+ canvas?.run {
+ save()
+ scale(
+ width.toFloat() / offscreenCanvasBitmap.width,
+ height.toFloat() / offscreenCanvasBitmap.height
+ )
+ drawBitmap(offscreenCanvasBitmap, 0f, 0f, null)
+ restore()
+ }
+ }
+
+ var emoji: CharSequence? = null
+ set(value) {
+ field = value
+ offscreenCanvasBitmap.eraseColor(Color.TRANSPARENT)
+ if (value != null) {
+ post {
+ drawEmoji(value)
+ invalidate()
+ }
+ }
+ }
+
+ private fun drawEmoji(emoji: CharSequence) {
+ if (emoji == this.emoji) {
+ offscreenCanvasBitmap.applyCanvas {
+ if (emoji is Spanned) {
+ createStaticLayout(emoji, width).draw(this)
+ } else {
+ val textWidth = textPaint.measureText(emoji, 0, emoji.length)
+ drawText(
+ emoji,
+ /* start = */ 0,
+ /* end = */ emoji.length,
+ /* x = */ (width - textWidth) / 2,
+ /* y = */ -textPaint.fontMetrics.top,
+ textPaint,
+ )
+ }
+ }
+ }
+ }
+
+ @RequiresApi(23)
+ internal object Api23Impl {
+ fun createStaticLayout(emoji: Spanned, textPaint: TextPaint, width: Int): StaticLayout =
+ StaticLayout.Builder.obtain(
+ emoji, 0, emoji.length, textPaint, width
+ ).apply {
+ setAlignment(Layout.Alignment.ALIGN_CENTER)
+ setLineSpacing(/* spacingAdd = */ 0f, /* spacingMult = */ 1f)
+ setIncludePad(false)
+ }.build()
+ }
+
+ private fun createStaticLayout(emoji: Spanned, width: Int): StaticLayout {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ return Api23Impl.createStaticLayout(emoji, textPaint, width)
+ } else {
+ @Suppress("DEPRECATION")
+ return StaticLayout(
+ emoji,
+ textPaint,
+ width,
+ Layout.Alignment.ALIGN_CENTER,
+ /* spacingmult = */ 1f,
+ /* spacingadd = */ 0f,
+ /* includepad = */ false,
+ )
+ }
+ }
+}
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 1b19f8d..317485e 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -1452,9 +1452,17 @@
* Asserts that {@code expectedImageFile} and {@code actualImageFile} can be decoded by
* {@link BitmapFactory} and the results have the same width, height and MIME type.
*
+ * <p>The assertion is skipped if the test is running on an API level where
+ * {@link BitmapFactory} is known not to support the image format of {@code expectedImageFile}
+ * (as determined by file extension).
+ *
* <p>This does not check the image itself for similarity/equality.
*/
private void assertBitmapsEquivalent(File expectedImageFile, File actualImageFile) {
+ if (Build.VERSION.SDK_INT < 16 && expectedImageFile.getName().endsWith("webp")) {
+ // BitmapFactory can't parse WebP files on API levels before 16: b/254571189
+ return;
+ }
BitmapFactory.Options expectedOptions = new BitmapFactory.Options();
Bitmap expectedBitmap = Objects.requireNonNull(
decodeBitmap(expectedImageFile, expectedOptions));
diff --git a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
index 0a38bcc..f32df12 100644
--- a/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
+++ b/fragment/fragment-testing/src/main/java/androidx/fragment/app/testing/FragmentScenario.kt
@@ -94,8 +94,8 @@
*
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param factory a fragment factory to use or null to use default factory
*/
public inline fun <reified F : Fragment> launchFragment(
@@ -116,8 +116,8 @@
*
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param instantiate method which will be used to instantiate the Fragment.
*/
public inline fun <reified F : Fragment> launchFragment(
@@ -146,8 +146,8 @@
*
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param factory a fragment factory to use or null to use default factory
*/
public inline fun <reified F : Fragment> launchFragmentInContainer(
@@ -169,8 +169,8 @@
*
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState the initial [Lifecycle.State]. This must be one of
- * [Lifecycle.State.CREATED], [Lifecycle.State.STARTED], or [Lifecycle.State.RESUMED].
+ * @param initialState the initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param instantiate method which will be used to instantiate the Fragment. This is a
* simplification of the [FragmentFactory] interface for cases where only a single class
* needs a custom constructor called.
@@ -304,9 +304,7 @@
* Moves Fragment state to a new state.
*
* If a new state and current state are the same, this method does nothing. It accepts
- * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED],
- * [RESUMED][Lifecycle.State.RESUMED], and [DESTROYED][Lifecycle.State.DESTROYED].
- * [DESTROYED][Lifecycle.State.DESTROYED] is a terminal state.
+ * all [Lifecycle.State]s. [DESTROYED][Lifecycle.State.DESTROYED] is a terminal state.
* You cannot move to any other state after the Fragment reaches that state.
*
* This method cannot be called from the main thread.
@@ -463,9 +461,8 @@
* @param fragmentClass a fragment class to instantiate
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState The initial [Lifecycle.State]. This must be one of
- * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED], and
- * [RESUMED][Lifecycle.State.RESUMED].
+ * @param initialState The initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param factory a fragment factory to use or null to use default factory
*/
@JvmOverloads
@@ -545,9 +542,8 @@
* @param fragmentClass a fragment class to instantiate
* @param fragmentArgs a bundle to passed into fragment
* @param themeResId a style resource id to be set to the host activity's theme
- * @param initialState The initial [Lifecycle.State]. This must be one of
- * [CREATED][Lifecycle.State.CREATED], [STARTED][Lifecycle.State.STARTED], and
- * [RESUMED][Lifecycle.State.RESUMED].
+ * @param initialState The initial [Lifecycle.State]. Passing in
+ * [DESTROYED][Lifecycle.State.DESTROYED] will result in an [IllegalArgumentException].
* @param factory a fragment factory to use or null to use default factory
*/
@JvmOverloads
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index e067485..d07f061 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -19,7 +19,6 @@
defaultConfig {
multiDexEnabled true
- testInstrumentationRunnerArgument "listener", "leakcanary.FailTestOnLeakRunListener"
}
namespace "androidx.fragment"
}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
index ea1aa11..0235efe 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ActivityLeakTest.kt
@@ -23,11 +23,13 @@
import androidx.testutils.runOnUiThreadRethrow
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertWithMessage
+import java.lang.ref.WeakReference
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
-import java.lang.ref.WeakReference
/**
* Class representing the different configurations for testing leaks
@@ -83,9 +85,13 @@
}
@Suppress("DEPRECATION")
- @get:Rule
val activityRule = androidx.test.rule.ActivityTestRule(ActivityLeakActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun testActivityDoesNotLeak() {
// Restart the activity because activityRule keeps a strong reference to the
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
index ce2a35a..bac35d4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordStateTest.kt
@@ -24,20 +24,27 @@
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class BackStackRecordStateTest {
- @get:Rule
+ @Suppress("DEPRECATION")
val activityRule = ActivityScenarioRule(EmptyFragmentTestActivity::class.java)
private val fragmentManager get() = activityRule.withActivity {
supportFragmentManager
}
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun testParcel() {
val fragment = StrictFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
index ce059cb..de1a619 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackRecordTest.kt
@@ -24,9 +24,11 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@@ -34,8 +36,12 @@
class BackStackRecordTest {
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Test
@UiThreadTest
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 c3025c4..25584e9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/BackStackStateTest.kt
@@ -25,20 +25,27 @@
import androidx.test.filters.LargeTest
import androidx.testutils.withActivity
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class BackStackStateTest {
- @get:Rule
+ @Suppress("DEPRECATION")
val activityRule = ActivityScenarioRule(EmptyFragmentTestActivity::class.java)
private val fragmentManager get() = activityRule.withActivity {
supportFragmentManager
}
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun testRestoreFromPending() {
val fragment = StrictFragment()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
index 0346592..f9ed241 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ChildFragmentStateTest.kt
@@ -22,17 +22,24 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class ChildFragmentStateTest {
+
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Test
@UiThreadTest
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
index c7314e8..a69f121 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ContentViewTest.kt
@@ -23,6 +23,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,6 +32,9 @@
@MediumTest
class ContentViewTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testContentViewWithInflatedFragment() {
// Test basic lifecycle when the fragment view is inflated with <fragment> tag
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
index 9951e7f..80f2dcb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DefaultSpecialEffectsControllerTest.kt
@@ -24,6 +24,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
@@ -31,6 +33,10 @@
@RunWith(AndroidJUnit4::class)
@MediumTest
class DefaultSpecialEffectsControllerTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun fragmentManagerGetSetSpecialEffectsController() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index 8ae92e1..8debe9d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -26,12 +26,14 @@
import androidx.lifecycle.LifecycleEventObserver
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
-import org.junit.runners.Parameterized
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
/**
* Class representing the different ways of dismissing a [DialogFragment]
@@ -99,10 +101,13 @@
}
@Suppress("DEPRECATION")
+ val activityTestRule =
+ androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityTestRule = androidx.test.rule.ActivityTestRule(
- EmptyFragmentTestActivity::class.java
- )
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityTestRule)
@Test
fun testDialogFragmentDismiss() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
index 7e427d56..18c0ce4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentInflatedChildTest.kt
@@ -28,6 +28,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -79,6 +81,9 @@
}
}
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testInflatedChildDialogFragment() {
withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
index 7e025db..5a8a3cd 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentTest.kt
@@ -38,18 +38,24 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class DialogFragmentTest {
+
@Suppress("DEPRECATION")
+ val activityTestRule =
+ androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- val activityTestRule = androidx.test.rule.ActivityTestRule(
- EmptyFragmentTestActivity::class.java
- )
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityTestRule)
@Test
fun testDialogFragmentShows() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
index ba52e8d..b4882b5 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentViewTreeTest.kt
@@ -29,12 +29,18 @@
import androidx.test.filters.LargeTest
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class DialogFragmentViewTreeTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testDialogFragmentViewTree() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
index 84b9f47..cc28826 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/EpicenterTest.kt
@@ -29,8 +29,10 @@
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@SmallTest
@@ -39,9 +41,13 @@
class EpicenterTest {
@Suppress("DEPRECATION")
- @get:Rule
val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun defaultEpicenter() {
testViewEpicenter {}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
index 3af0369..99caf17 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentActivityResultTest.kt
@@ -32,7 +32,9 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -43,6 +45,9 @@
@MediumTest
class FragmentActivityResultTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun registerActivityResultInOnAttach() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
index 4350b4f..be9369c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatedContainerTest.kt
@@ -22,19 +22,26 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(AndroidJUnit4::class)
class FragmentAnimatedContainerTest {
+
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Before
fun setupContainer() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
index 69a2491..d9480fe 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentAnimatorTest.kt
@@ -35,20 +35,26 @@
import androidx.test.filters.LargeTest
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(AndroidJUnit4::class)
class FragmentAnimatorTest {
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Before
fun setupContainer() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
index b9e79ec..18af166 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentArchLifecycleTest.kt
@@ -30,6 +30,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,6 +39,9 @@
@LargeTest
class FragmentArchLifecycleTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testFragmentAdditionDuringOnStop() {
withUse(ActivityScenario.launch(EmptyFragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
index 436b3d4..81cf605 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentContainerInflatedFragmentTest.kt
@@ -27,6 +27,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,6 +36,9 @@
@LargeTest
class FragmentContainerInflatedFragmentTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testContentViewWithInflatedFragment() {
// The StrictViewFragment runs the appropriate checks to make sure
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
index 68ae0531..089d11e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFinishEarlyTest.kt
@@ -21,24 +21,30 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class FragmentFinishEarlyTest {
@Suppress("DEPRECATION")
- @get:Rule
val activityRule = androidx.test.rule.ActivityTestRule(
FragmentFinishEarlyTestActivity::class.java,
false,
false
)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
/**
* FragmentActivity should not raise the state of a Fragment while it is being destroyed.
*/
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
index 28fe352..74c143b 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentFocusTest.kt
@@ -31,15 +31,20 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
@LargeTest
@RunWith(AndroidJUnit4::class)
class FragmentFocusTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun focusedViewRemoved() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
index d31fa72..aedeb6a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentLifecycleTest.kt
@@ -42,9 +42,11 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
@@ -55,9 +57,13 @@
class FragmentLifecycleTest {
@Suppress("DEPRECATION")
- @get:Rule
val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun basicLifecycle() {
val fm = activityRule.activity.supportFragmentManager
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
index 1e72738..4221075 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerNonConfigTest.kt
@@ -23,8 +23,10 @@
import androidx.test.filters.SdkSuppress
import androidx.testutils.recreate
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@MediumTest
@@ -32,9 +34,13 @@
class FragmentManagerNonConfigTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(NonConfigOnStopActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
/**
* When a fragment is added during onStop(), it shouldn't show up in non-config
* state when restored before P, because OnSaveInstanceState was already called.
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
index 8fa1d0d..20705fd 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerSavedStateRegistryTest.kt
@@ -23,6 +23,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -30,6 +32,9 @@
@MediumTest
class FragmentManagerSavedStateRegistryTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
@Throws(Throwable::class)
fun savedState() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
index a062eea..72dcbb3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -24,7 +24,9 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -32,6 +34,9 @@
@RunWith(AndroidJUnit4::class)
class FragmentManagerTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun addRemoveFragmentOnAttachListener() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
index e17ea7a..8b9fc41 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerViewModelTest.kt
@@ -20,7 +20,9 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
@@ -29,6 +31,9 @@
@LargeTest
class FragmentManagerViewModelTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
private lateinit var viewModel: FragmentManagerViewModel
@Before
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
index 6606f46..1b0f929 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentReplaceTest.kt
@@ -25,6 +25,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +37,9 @@
@LargeTest
class FragmentReplaceTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testReplaceFragment() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
index a73efd9..a7112a1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentResultTest.kt
@@ -28,6 +28,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +37,9 @@
@LargeTest
class FragmentResultTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testReplaceResult() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
index 3df365a..c015eff 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
@@ -34,6 +34,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +43,9 @@
@LargeTest
class FragmentSavedStateRegistryTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
private fun ActivityScenario<FragmentSavedStateActivity>.initializeSavedState(
testFragment: Fragment = Fragment()
) = withActivity {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
index c6ae071..4bd91d9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSharedElementTransitionTest.kt
@@ -27,6 +27,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,6 +37,9 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
class FragmentSharedElementTransitionTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testNestedSharedElementView() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
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 1669ac0..1af6505 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStateManagerTest.kt
@@ -21,7 +21,9 @@
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -38,6 +40,9 @@
private val classLoader get() = InstrumentationRegistry.getInstrumentation()
.targetContext.classLoader
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Before
fun setup() {
fragmentStore = FragmentStore()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
index c5a62e7..2313a6f 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTest.kt
@@ -32,24 +32,31 @@
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicInteger
/**
* Miscellaneous tests for fragments that aren't big enough to belong to their own classes.
*/
@RunWith(AndroidJUnit4::class)
class FragmentTest {
+
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
private lateinit var instrumentation: Instrumentation
@Before
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
index e779648..3925588 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransactionTest.kt
@@ -32,14 +32,16 @@
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.After
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
/**
* Tests usage of the [androidx.fragment.app.FragmentTransaction] class.
@@ -49,9 +51,13 @@
class FragmentTransactionTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
private var onBackStackChangedTimes: Int = 0
private lateinit var onBackStackChangedListener: FragmentManager.OnBackStackChangedListener
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
index 0f9a36a..1a0a8a1 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitTest.kt
@@ -27,9 +27,11 @@
import androidx.test.filters.LargeTest
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
/**
@@ -38,10 +40,15 @@
@LargeTest
@RunWith(AndroidJUnit4::class)
class FragmentTransitTest {
+
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Before
fun setupContainer() {
activityRule.setContentView(R.layout.simple_container)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
index 9fb383b..ed669b4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionAnimTest.kt
@@ -32,12 +32,14 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
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.CountDownLatch
-import java.util.concurrent.TimeUnit
@LargeTest
@RunWith(Parameterized::class)
@@ -47,6 +49,9 @@
) {
private var onBackStackChangedTimes: Int = 0
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Before
fun setup() {
>
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
index 4796cac..3da8c66 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/HangingFragmentTest.kt
@@ -21,8 +21,10 @@
import androidx.test.filters.MediumTest
import androidx.testutils.recreate
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@@ -30,9 +32,13 @@
class HangingFragmentTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(HangingFragmentActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun testNoCrash() {
val newActivity = activityRule.recreate()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
index d94385c..97f5ae8 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/LoaderTest.kt
@@ -28,18 +28,25 @@
import androidx.testutils.recreate
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
+import java.lang.ref.WeakReference
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.lang.ref.WeakReference
@RunWith(AndroidJUnit4::class)
@MediumTest
class LoaderTest {
+
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(LoaderActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
/**
* Test to ensure that there is no Activity leak due to Loader
*/
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
index b6c042c..e9c49dc 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/MenuVisibilityFragmentTest.kt
@@ -24,6 +24,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -31,6 +33,9 @@
@RunWith(AndroidJUnit4::class)
class MenuVisibilityFragmentTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun setMenuVisibility() {
withUse(ActivityScenario.launch(SimpleContainerActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
index c98d7a7..88e79ea 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentRestoreTest.kt
@@ -27,15 +27,20 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertWithMessage
-import org.junit.Test
-import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
class NestedFragmentRestoreTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Suppress("DEPRECATION")
@Test
fun recreateActivity() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
index 6431a8d..2e65beb 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedInflatedFragmentTest.kt
@@ -28,8 +28,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@@ -37,9 +39,13 @@
class NestedInflatedFragmentTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
@UiThreadTest
fun inflatedChildFragment() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
index 578f168..241a570d 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
@@ -29,6 +29,9 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,6 +40,9 @@
@MediumTest
class OnBackPressedCallbackTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun testBackPressFinishesActivity() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
@@ -167,6 +173,7 @@
}
}
+ @Ignore // b/250870927
@Test
fun testBackPressFinishesActivityAfterFragmentPop() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
index 3cdf8efd..27a4ec9 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OptionsMenuFragmentTest.kt
@@ -40,8 +40,10 @@
import com.google.common.truth.Truth.assertWithMessage
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O) // Previous SDK levels have issues passing
@@ -49,10 +51,15 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class OptionsMenuFragmentTest {
+
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun fragmentWithOptionsMenu() {
activityRule.setContentView(R.layout.simple_container)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
index 4255529..beb4c0a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PrimaryNavFragmentTest.kt
@@ -27,17 +27,24 @@
import androidx.testutils.waitForExecution
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class PrimaryNavFragmentTest {
+
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
fun delegateBackToPrimaryNav() {
val fm = activityRule.activity.supportFragmentManager
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
index a9de786..a8992af 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ReentrantFragmentTest.kt
@@ -22,9 +22,11 @@
import androidx.lifecycle.ViewModelStore
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -50,8 +52,12 @@
}
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
// Make sure that executing transactions during activity lifecycle events
// is properly prevented.
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
index 8b58e97..a24708e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SafeTransactionInOnResumeTest.kt
@@ -23,23 +23,29 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
@LargeTest
class SafeTransactionInOnResumeTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule<OnResumeTestActivity>(
OnResumeTestActivity::class.java
)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@Test
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
fun onResumeTest() {
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 7ab46c8..413ec73 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveRestoreBackStackTest.kt
@@ -26,7 +26,9 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert.fail
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,6 +36,9 @@
@RunWith(AndroidJUnit4::class)
class SaveRestoreBackStackTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun saveBackStack() {
withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
index 4ea17b2..f9bcff3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SaveStateFragmentTest.kt
@@ -26,8 +26,10 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@@ -35,8 +37,12 @@
class SaveStateFragmentTest {
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Test
@UiThreadTest
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
index 0b9d741..123bcf4 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/SpecialEffectsControllerTest.kt
@@ -28,12 +28,18 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@SmallTest
class SpecialEffectsControllerTest {
+
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test
fun factoryCreateController() {
val map = mutableMapOf<ViewGroup, TestSpecialEffectsController>()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
index b11ab9c..15e5a3c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/TargetFragmentLifeCycleTest.kt
@@ -24,9 +24,11 @@
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@Suppress("DEPRECATION")
@@ -35,8 +37,12 @@
class TargetFragmentLifeCycleTest {
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Test
fun targetFragmentNoCycles() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
index 14b4071..7613e37 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/UserVisibleHintTest.kt
@@ -22,8 +22,10 @@
import androidx.test.filters.MediumTest
import androidx.testutils.runOnUiThreadRethrow
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@Suppress("DEPRECATION")
@@ -32,9 +34,13 @@
class UserVisibleHintTest {
@Suppress("DEPRECATION")
- @get:Rule
var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
+ // Detect leaks BEFORE and AFTER activity is destroyed
+ @get:Rule
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
+
@UiThreadTest
@Test
fun startOrdering() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
index fe34c2d..7fc4207 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTest.kt
@@ -29,6 +29,8 @@
import androidx.testutils.withActivity
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -36,6 +38,9 @@
@RunWith(AndroidJUnit4::class)
class ViewModelTest {
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Test(expected = IllegalStateException::class)
@UiThreadTest
fun testNotAttachedFragment() {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
index 9809d911..dcbb91a 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ViewModelTestInTransaction.kt
@@ -24,8 +24,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.Rule
import org.junit.Test
+import org.junit.rules.RuleChain
import org.junit.runner.RunWith
@SmallTest
@@ -33,8 +35,12 @@
class ViewModelTestInTransaction {
@Suppress("DEPRECATION")
+ val activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+
+ // Detect leaks BEFORE and AFTER activity is destroyed
@get:Rule
- var activityRule = androidx.test.rule.ActivityTestRule(EmptyFragmentTestActivity::class.java)
+ val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+ .around(activityRule)
@Test
@UiThreadTest
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 f23e8b3..83a2f21 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
@@ -29,8 +29,10 @@
import androidx.testutils.withUse
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,6 +43,9 @@
private lateinit var originalPolicy: FragmentStrictMode.Policy
+ @get:Rule
+ val rule = DetectLeaksAfterTestSuccess()
+
@Before
public fun setup() {
originalPolicy = FragmentStrictMode.defaultPolicy
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
index 811b311..189229d 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/AndroidManifest.xml
@@ -45,7 +45,7 @@
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
- android:resource="@xml/default_app_widget_info" />
+ android:resource="@xml/gallery_app_widget_info" />
</receiver>
<receiver
@@ -59,7 +59,7 @@
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
- android:resource="@xml/default_app_widget_info" />
+ android:resource="@xml/gallery_app_widget_info" />
</receiver>
<receiver
@@ -73,7 +73,7 @@
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
- android:resource="@xml/default_app_widget_info" />
+ android:resource="@xml/gallery_app_widget_info" />
</receiver>
<receiver
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
index 790285e..bbf0c0b 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/GalleryDemoWidget.kt
@@ -21,7 +21,6 @@
import androidx.glance.ImageProvider
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import androidx.glance.appwidget.SizeMode
import androidx.glance.appwidget.action.actionRunCallback
import androidx.glance.appwidget.template.GalleryTemplate
import androidx.glance.appwidget.template.GlanceTemplateAppWidget
@@ -82,7 +81,6 @@
* It is overridable by gallery image aspect ratio, image size, and main blocks ordering.
*/
abstract class BaseGalleryTemplateWidget : GlanceTemplateAppWidget() {
- override val sizeMode = SizeMode.Exact
@Composable
internal fun GalleryTemplateContent(
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml
new file mode 100644
index 0000000..d4ebabd
--- /dev/null
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/res/xml/gallery_app_widget_info.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
+ android:minWidth="100dp"
+ android:minHeight="100dp"
+ android:minResizeHeight="40dp"
+ android:minResizeWidth="90dp"
+ android:initialLayout="@layout/glance_default_loading_layout"
+ android:previewImage="@drawable/palm_leaf"
+ android:resizeMode="horizontal|vertical"
+ android:widgetCategory="home_screen" />
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
index e8fb592..6963b7c 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
@@ -27,6 +27,7 @@
import androidx.glance.appwidget.lazy.GridCells
import androidx.glance.appwidget.lazy.LazyVerticalGrid
import androidx.glance.appwidget.lazy.itemsIndexed
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeMin
import androidx.glance.background
import androidx.glance.layout.Alignment
import androidx.glance.layout.Column
@@ -160,12 +161,17 @@
@Composable
private fun HeaderAndTextBlocks(data: GalleryTemplateData, modifier: GlanceModifier) {
Column(modifier = modifier) {
- HeaderBlockTemplate(data.header)
- // TODO(b/247613894): Spacing should be conditional
- Spacer(modifier = GlanceModifier.height(16.dp).defaultWeight())
+ data.header?.let {
+ HeaderBlockTemplate(data.header)
+ Spacer(modifier = GlanceModifier.height(16.dp).defaultWeight())
+ }
TextBlockTemplate(data.mainTextBlock)
- Spacer(modifier = GlanceModifier.height(16.dp))
- ActionBlockTemplate(data.mainActionBlock)
+ data.mainActionBlock?.let {
+ if (LocalSize.current.width > sizeMin && LocalSize.current.height > sizeMin) {
+ Spacer(modifier = GlanceModifier.height(16.dp))
+ ActionBlockTemplate(data.mainActionBlock)
+ }
+ }
}
}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
index dfc7952..82b55d2 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
@@ -36,13 +36,17 @@
abstract class GlanceTemplateAppWidget : GlanceAppWidget() {
companion object {
- private val COLLAPSED = DpSize(30.dp, 30.dp)
- private val HORIZONTAL_S = DpSize(241.dp, 30.dp)
- private val HORIZONTAL_M = DpSize(241.dp, 200.dp)
- private val HORIZONTAL_L = DpSize(350.dp, 200.dp)
- private val VERTICAL_S = DpSize(30.dp, 241.dp)
- private val VERTICAL_M = DpSize(200.dp, 241.dp)
- private val VERTICAL_L = DpSize(200.dp, 350.dp)
+ internal val sizeMin = 30.dp
+ internal val sizeS = 200.dp
+ internal val sizeM = 241.dp
+ internal val sizeL = 350.dp
+ private val COLLAPSED = DpSize(sizeMin, sizeMin)
+ private val HORIZONTAL_S = DpSize(sizeM, sizeMin)
+ private val HORIZONTAL_M = DpSize(sizeM, sizeS)
+ private val HORIZONTAL_L = DpSize(sizeL, sizeS)
+ private val VERTICAL_S = DpSize(sizeMin, sizeM)
+ private val VERTICAL_M = DpSize(sizeS, sizeM)
+ private val VERTICAL_L = DpSize(sizeS, sizeL)
}
/** Default widget size mode is [SizeMode.Responsive] */
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
index f56c2e5..f50c2ec 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
@@ -22,6 +22,8 @@
import androidx.glance.GlanceTheme
import androidx.glance.LocalSize
import androidx.glance.appwidget.cornerRadius
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeMin
+import androidx.glance.appwidget.template.GlanceTemplateAppWidget.Companion.sizeS
import androidx.glance.background
import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
@@ -88,7 +90,7 @@
Row(modifier = GlanceModifier.fillMaxWidth()) {
data.textBlock?.let { AppWidgetTextSection(textList(it.text1, it.text2)) }
// TODO(b/247613894): Fix for multiple actions
- if (LocalSize.current.width > 100.dp) {
+ if (LocalSize.current.width > sizeMin) {
data.actionBlock?.let {
Spacer(modifier = GlanceModifier.width(16.dp))
Spacer(modifier = GlanceModifier.defaultWeight())
@@ -111,13 +113,11 @@
}
Spacer(modifier = GlanceModifier.defaultWeight())
- // TODO(b/247613894): Extract size constraints as template constants
data.textBlock?.let {
- val body = if (LocalSize.current.height > 150.dp) it.text3 else null
+ val body = if (LocalSize.current.height >= sizeS) it.text3 else null
AppWidgetTextSection(textList(it.text1, it.text2, body))
}
- // TODO(b/247613894): Extract threshold sizes as constants
- if (LocalSize.current.height > 30.dp) {
+ if (LocalSize.current.height > sizeMin) {
data.actionBlock?.let {
Spacer(modifier = GlanceModifier.height(16.dp))
ActionBlockTemplate(it)
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 30e291c..3d96ae5 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -57,6 +57,7 @@
testImplementation("androidx.datastore:datastore-core:1.0.0")
testImplementation("androidx.datastore:datastore-preferences-core:1.0.0")
testImplementation("androidx.datastore:datastore-preferences:1.0.0-rc02")
+ testImplementation("androidx.room:room-runtime:2.4.3")
testImplementation("androidx.work:work-testing:2.7.1")
testImplementation("com.google.android.material:material:1.6.0")
}
diff --git a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
index 8bc9546..8b3e4da 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/session/SessionManagerImplTest.kt
@@ -24,6 +24,7 @@
import androidx.work.CoroutineWorker
import androidx.work.WorkManager
import androidx.work.WorkerParameters
+import androidx.work.impl.WorkManagerImpl
import androidx.work.testing.WorkManagerTestInitHelper
import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.suspendCoroutine
@@ -31,7 +32,6 @@
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
@@ -69,9 +69,11 @@
@After
fun cleanUp() {
WorkManager.getInstance(context).cancelAllWork()
+ // TODO(b/242026176): remove this once WorkManager allows closing the test
+ // database.
+ WorkManagerImpl.getInstance(context).workDatabase.close()
}
- @Ignore // b/254048913
@Test
fun startSession() = runTest {
assertThat(sessionManager.isSessionRunning(context, key)).isFalse()
@@ -80,7 +82,6 @@
assertThat(sessionManager.getSession(key)).isSameInstanceAs(session)
}
- @Ignore // b/254048913
@Test
fun closeSession() = runTest {
sessionManager.startSession(context, session)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8967b76..f628ef5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,12 +14,12 @@
androidGradlePluginMin = "7.0.4"
androidLintMin = "30.0.4"
androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.4.0"
-androidxTestRules = "1.4.0"
-androidxTestMonitor = "1.5.0"
-androidxTestCore = "1.5.0-beta01"
-androidxTestExtJunit = "1.1.3"
-androidxTestExtTruth = "1.4.0"
+androidxTestRunner = "1.5.0-rc01"
+androidxTestRules = "1.5.0-rc01"
+androidxTestMonitor = "1.6.0-rc01"
+androidxTestCore = "1.5.0-rc01"
+androidxTestExtJunit = "1.1.4-rc01"
+androidxTestExtTruth = "1.5.0-rc01"
atomicFu = "0.17.0"
autoService = "1.0-rc6"
autoValue = "1.6.3"
@@ -29,7 +29,7 @@
dagger = "2.44"
dexmaker = "2.28.3"
dokka = "1.7.20"
-espresso = "3.5.0-beta01"
+espresso = "3.5.0-rc01"
guavaJre = "31.1-jre"
hilt = "2.44"
incap = "0.2"
@@ -46,7 +46,7 @@
metalava = "1.0.0-alpha06"
mockito = "2.25.0"
moshi = "1.13.0"
-protobuf = "3.19.4"
+protobuf = "3.21.8"
paparazzi = "1.0.0"
paparazziNative = "2022.1.1-canary-f5f9f71"
skiko = "0.7.7"
@@ -192,9 +192,12 @@
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
+playFeatureDelivery = { module = "com.google.android.play:feature-delivery", version = "2.0.1" }
playCore = { module = "com.google.android.play:core", version = "1.10.3" }
+playServicesAuth = {module = "com.google.android.gms:play-services-auth", version = "20.3.0"}
playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
+playServicesFido = {module = "com.google.android.gms:play-services-fido", version = "19.0.0-beta"}
playServicesWearable = { module = "com.google.android.gms:play-services-wearable", version = "17.1.0" }
paparazzi = { module = "app.cash.paparazzi:paparazzi", version.ref = "paparazzi" }
paparazziNativeJvm = { module = "app.cash.paparazzi:layoutlib-native-jdk11", version.ref = "paparazziNative" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f8abc10..c4aadd3 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -15,6 +15,7 @@
<trust group="com.google.android.gms"/> <!-- b/215442095 -->
<trust group="com.google.android.datatransport"/> <!-- b/215442095 -->
<trust group="com.google.android.material"/> <!-- b/216192082 -->
+ <trust group="com.google.android.play"/> <!-- b/255621183 -->
<trust group="com.google.firebase"/> <!-- b/223907608 -->
<trust group="com.google.mlkit"/> <!-- b/223907608 -->
<trust file=".*kotlin-native-prebuilt-macos-.*" regex="true"/> <!-- b/228184608 -->
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 430083d..63f1c4e 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -21,11 +21,9 @@
import android.opengl.GLES20
import android.opengl.Matrix
import android.os.Build
-import android.util.Log
import android.view.SurfaceView
import androidx.annotation.RequiresApi
import androidx.graphics.opengl.egl.EGLManager
-import androidx.graphics.opengl.egl.deviceSupportsNativeAndroidFence
import androidx.graphics.surface.SurfaceControlCompat
import androidx.graphics.surface.SurfaceControlUtils
import androidx.lifecycle.Lifecycle
@@ -54,14 +52,12 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testFrontBufferedLayerRender() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding extensions to create
- // a file descriptor from an EGLSync object then skip the test
- Log.w(TAG, "Skipping testFrontBufferedLayerRender, no native android fence support")
- return
- }
val renderLatch = CountDownLatch(1)
val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+ val mProjectionMatrix = FloatArray(16)
+ val mOrthoMatrix = FloatArray(16)
+
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferWidth: Int,
@@ -70,8 +66,18 @@
param: Any
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
}
override fun onDrawDoubleBufferedLayer(
@@ -82,8 +88,18 @@
params: Collection<Any>
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
}
override fun onFrontBufferedLayerRenderComplete(
@@ -140,15 +156,12 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
fun testDoubleBufferedLayerRender() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding extensions to create
- // a file descriptor from an EGLSync object then skip the test
- Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
- return
- }
-
val renderLatch = CountDownLatch(1)
val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+ private val mOrthoMatrix = FloatArray(16)
+ private val mProjectionMatrix = FloatArray(16)
+
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferWidth: Int,
@@ -157,8 +170,18 @@
param: Any
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
}
override fun onDrawDoubleBufferedLayer(
@@ -169,8 +192,18 @@
params: Collection<Any>
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
}
override fun onDoubleBufferedLayerRenderComplete(
@@ -287,17 +320,16 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testRenderFrontBufferSeveralTimes() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding extensions to create
- // a file descriptor from an EGLSync object then skip the test
- Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
- return
- }
val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
var red = 1f
var blue = 0f
+ val mOrthoMatrix = FloatArray(16)
+ val mProjectionMatrix = FloatArray(16)
+ var mRectangle: Rectangle? = null
+
+ private fun getSquare(): Rectangle = mRectangle ?: Rectangle().also { mRectangle = it }
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
@@ -307,8 +339,20 @@
param: Any
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(red, 0.0f, blue, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ val color = Color.argb(1f, red, 0f, blue)
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ getSquare().draw(mProjectionMatrix, color, 0f, 0f, 100f, 100f)
+
val tmp = red
red = blue
blue = tmp
@@ -322,8 +366,19 @@
params: Collection<Any>
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
- GLES20.glClearColor(0.0f, 0.0f, 1.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ val color = Color.argb(1f, red, 0f, blue)
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ getSquare().draw(mProjectionMatrix, color, 0f, 0f, 100f, 100f)
}
}
var renderer: GLFrontBufferedRenderer<Any>? = null
@@ -336,7 +391,7 @@
scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
val param = Any()
- repeat(4000) {
+ repeat(500) {
renderer?.renderFrontBufferedLayer(param)
}
}
@@ -347,13 +402,7 @@
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
@Test
- fun testFrontBufferedLayerPersistence() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding extensions to create
- // a file descriptor from an EGLSync object then skip the test
- Log.w(TAG, "Skipping testDoubleBufferedLayerRender, no native android fence support")
- return
- }
+ fun testDoubleBufferedContentsNotPersisted() {
val mOrthoMatrix = FloatArray(16)
val mProjectionMatrix = FloatArray(16)
val mLines = FloatArray(4)
@@ -474,9 +523,9 @@
SurfaceControlUtils.validateOutput { bitmap ->
(bitmap.getPixel(
coords[0] + width / 4, coords[1] + height / 2
- ) == Color.RED) &&
+ ) == Color.BLACK) &&
(bitmap.getPixel(
- coords[0] + 3 * width / 4,
+ coords[0] + 3 * width / 4 - 1,
coords[1] + height / 2
) == Color.RED)
}
@@ -485,6 +534,116 @@
}
}
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+ @Test
+ fun testRenderAfterPauseAndResume() {
+ val renderLatch = CountDownLatch(2)
+ val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+ val mProjectionMatrix = FloatArray(16)
+ val mOrthoMatrix = FloatArray(16)
+
+ override fun onDrawFrontBufferedLayer(
+ eglManager: EGLManager,
+ bufferWidth: Int,
+ bufferHeight: Int,
+ transform: FloatArray,
+ param: Any
+ ) {
+ GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+ }
+
+ override fun onDrawDoubleBufferedLayer(
+ eglManager: EGLManager,
+ bufferWidth: Int,
+ bufferHeight: Int,
+ transform: FloatArray,
+ params: Collection<Any>
+ ) {
+ GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ bufferWidth.toFloat(),
+ 0f,
+ bufferHeight.toFloat(),
+ -1f,
+ 1f
+ )
+ Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+ Rectangle().draw(mProjectionMatrix, Color.BLUE, 0f, 0f, 100f, 100f)
+ }
+
+ override fun onFrontBufferedLayerRenderComplete(
+ frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+ transaction: SurfaceControlCompat.Transaction
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ transaction.addTransactionCommittedListener(
+ Executors.newSingleThreadExecutor(),
+ object : SurfaceControlCompat.TransactionCommittedListener {
+ override fun onTransactionCommitted() {
+ renderLatch.countDown()
+ }
+ }
+ )
+ } else {
+ renderLatch.countDown()
+ }
+ }
+ }
+ var renderer: GLFrontBufferedRenderer<Any>? = null
+ var surfaceView: SurfaceView? = null
+ try {
+ val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+ .moveToState(Lifecycle.State.CREATED)
+ .onActivity {
+ surfaceView = it.getSurfaceView()
+ renderer = GLFrontBufferedRenderer(surfaceView!!, callbacks)
+ }
+
+ scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+ renderer?.renderFrontBufferedLayer(Any())
+ }
+ // Navigate to stopped and resumed state to simulate returning to the application
+ scenario.moveToState(Lifecycle.State.CREATED)
+ .moveToState(Lifecycle.State.RESUMED)
+ .onActivity {
+ renderer?.renderFrontBufferedLayer(Any())
+ }
+ assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
+
+ val coords = IntArray(2)
+ val width: Int
+ val height: Int
+ with(surfaceView!!) {
+ getLocationOnScreen(coords)
+ width = this.width
+ height = this.height
+ }
+
+ SurfaceControlUtils.validateOutput { bitmap ->
+ Color.RED ==
+ bitmap.getPixel(coords[0] + width / 2, coords[1] + height / 2)
+ }
+ } finally {
+ renderer.blockingRelease()
+ }
+ }
+
@RequiresApi(Build.VERSION_CODES.Q)
private fun GLFrontBufferedRenderer<*>?.blockingRelease(timeoutMillis: Long = 3000) {
if (this != null) {
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
similarity index 67%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
copy to graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
index 79122bf..04d04be 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLUtils.kt
@@ -14,10 +14,15 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.graphics.lowlatency
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
+import android.opengl.GLES20
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+fun loadShader(type: Int, shaderCode: String?): Int {
+ val shader = GLES20.glCreateShader(type)
+
+ GLES20.glShaderSource(shader, shaderCode)
+ GLES20.glCompileShader(shader)
+
+ return shader
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
index 9cd1708..0e5642c 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/InkSurfaceView.kt
@@ -59,6 +59,8 @@
private val mMVPMatrix = FloatArray(16)
private val mProjection = FloatArray(16)
+ private val mSceneParams = ArrayList<FloatArray>()
+
override fun onDrawFrontBufferedLayer(
eglManager: EGLManager,
bufferWidth: Int,
@@ -86,7 +88,7 @@
drawLines(mProjection, floatArrayOf(0f, vHeight, vWidth, vHeight), Color.CYAN)
drawLines(mProjection, floatArrayOf(vWidth, vHeight, vWidth, 0f), Color.BLUE)
drawLines(mProjection, floatArrayOf(vWidth, 0f, 0f, 0f), Color.MAGENTA)
- drawLines(mProjection, param, Color.RED, 20f)
+ drawLines(mProjection, param, Color.YELLOW, LINE_WIDTH)
}
}
@@ -98,6 +100,8 @@
params: Collection<FloatArray>
) {
GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+ GLES20.glClearColor(0f, 0f, 0f, 0f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
Matrix.orthoM(
mMVPMatrix,
0,
@@ -109,8 +113,9 @@
1f
)
Matrix.multiplyMM(mProjection, 0, mMVPMatrix, 0, transform, 0)
- for (line in params) {
- obtainRenderer().drawLines(mProjection, line, Color.BLUE, 20f)
+ mSceneParams.addAll(params)
+ for (line in mSceneParams) {
+ obtainRenderer().drawLines(mProjection, line, Color.BLUE, LINE_WIDTH)
}
}
}
@@ -171,4 +176,8 @@
}
super.onDetachedFromWindow()
}
+
+ private companion object {
+ private const val LINE_WIDTH = 5f
+ }
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
index e50d0ca56..363422b 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/LineRenderer.kt
@@ -156,14 +156,5 @@
gl_FragColor = $vColor;
}
"""
-
- fun loadShader(type: Int, shaderCode: String?): Int {
- val shader = GLES20.glCreateShader(type)
-
- GLES20.glShaderSource(shader, shaderCode)
- GLES20.glCompileShader(shader)
-
- return shader
- }
}
}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt
new file mode 100644
index 0000000..5ac0c6c
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/ParamQueueTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.lowlatency
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+internal class ParamQueueTest {
+
+ @Test
+ fun testAdd() {
+ val queue = ParamQueue<Int>()
+ queue.add(1)
+ queue.next {
+ assertEquals(1, it)
+ }
+ }
+
+ @Test
+ fun testNextWithEmptyQueue() {
+ var blockInvoked = false
+ ParamQueue<Int>().next { blockInvoked = true }
+ // empty queue should not call this lambda
+ assertFalse(blockInvoked)
+ }
+
+ @Test
+ fun testRelease() {
+ val queue = ParamQueue<Int>()
+ with(queue) {
+ for (i in 1 until 5) {
+ add(i)
+ }
+ }
+ val list = queue.release()
+ var count = 1
+ for (entry in list) {
+ assertEquals(count++, entry)
+ }
+ }
+
+ @Test
+ fun testClear() {
+ with(ParamQueue<Int>()) {
+ for (i in 0 until 2) {
+ add(i)
+ }
+ clear()
+ val list = release()
+ assertTrue(list.isEmpty())
+ }
+ }
+
+ @Test
+ fun testNext() {
+ with(ParamQueue<Int>()) {
+ for (i in 0 until 10) {
+ add(i)
+ }
+ var count = 0
+ for (i in 0 until 10) {
+ next { it ->
+ assertEquals(i, it)
+ count++
+ }
+ }
+ assertEquals(10, count)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt
new file mode 100644
index 0000000..2b8b469
--- /dev/null
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/Rectangle.kt
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.graphics.lowlatency
+
+import android.graphics.Color
+import android.opengl.GLES20
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+import java.nio.ShortBuffer
+
+class Rectangle {
+
+ private val mVertexBuffer: FloatBuffer
+ private val mDrawListBuffer: ShortBuffer
+ private val mProgram: Int
+ private var mPositionHandle = 0
+ private var mColorHandle = 0
+ private var mMVPMatrixHandle = 0
+ private var mColor = floatArrayOf(1f, 0f, 0f, 1f)
+ private var mSquareCoords = FloatArray(12)
+
+ /**
+ * Sets up the drawing object data for use in an OpenGL ES context.
+ */
+ init {
+ // initialize vertex byte buffer for shape coordinates
+ val bb = ByteBuffer.allocateDirect( // (# of coordinate values * 4 bytes per float)
+ mSquareCoords.size * 4
+ )
+ bb.order(ByteOrder.nativeOrder())
+ mVertexBuffer = bb.asFloatBuffer()
+ mVertexBuffer.put(mSquareCoords)
+ mVertexBuffer.position(0)
+ // initialize byte buffer for the draw list
+ val dlb = ByteBuffer.allocateDirect( // (# of coordinate values * 2 bytes per short)
+ DRAW_ORDER.size * 2
+ )
+ dlb.order(ByteOrder.nativeOrder())
+ mDrawListBuffer = dlb.asShortBuffer()
+ mDrawListBuffer.put(DRAW_ORDER)
+ mDrawListBuffer.position(0)
+ // prepare shaders and OpenGL program
+ val vertexShader = loadShader(
+ GLES20.GL_VERTEX_SHADER,
+ vertexShaderCode
+ )
+ val fragmentShader = loadShader(
+ GLES20.GL_FRAGMENT_SHADER,
+ fragmentShaderCode
+ )
+ mProgram = GLES20.glCreateProgram() // create empty OpenGL Program
+ GLES20.glAttachShader(mProgram, vertexShader) // add the vertex shader to program
+ GLES20.glAttachShader(mProgram, fragmentShader) // add the fragment shader to program
+ GLES20.glLinkProgram(mProgram) // create OpenGL program executables
+ }
+
+ fun draw(
+ mvpMatrix: FloatArray?,
+ color: Int,
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float
+ ) {
+
+ mColor[0] = Color.red(color) / 255f
+ mColor[1] = Color.green(color) / 255f
+ mColor[2] = Color.blue(color) / 255f
+ mColor[3] = Color.alpha(color) / 255f
+
+ // top left
+ mSquareCoords[0] = left
+ mSquareCoords[1] = top
+ mSquareCoords[2] = 0f
+ // bottom left
+ mSquareCoords[3] = left
+ mSquareCoords[4] = bottom
+ mSquareCoords[5] = 0f
+ // bottom right
+ mSquareCoords[6] = right
+ mSquareCoords[7] = bottom
+ mSquareCoords[8] = 0f
+ // top right
+ mSquareCoords[9] = right
+ mSquareCoords[10] = top
+ mSquareCoords[11] = 0f
+
+ mVertexBuffer.clear()
+ mVertexBuffer.put(mSquareCoords)
+ mVertexBuffer.position(0)
+
+ GLES20.glUseProgram(mProgram)
+ // get handle to vertex shader's vPosition member
+ mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition")
+ // Enable a handle to the triangle vertices
+ GLES20.glEnableVertexAttribArray(mPositionHandle)
+ // Prepare the triangle coordinate data
+ // 4 bytes per vertex
+ val vertexStride = COORDS_PER_VERTEX * 4
+ GLES20.glVertexAttribPointer(
+ mPositionHandle, COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT, false,
+ vertexStride, mVertexBuffer
+ )
+ // get handle to fragment shader's vColor member
+ mColorHandle = GLES20.glGetUniformLocation(mProgram, "vColor")
+ // Set color for drawing the triangle
+ GLES20.glUniform4fv(mColorHandle, 1, mColor, 0)
+ // get handle to shape's transformation matrix
+ mMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix")
+ // Apply the projection and view transformation
+ GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0)
+ // Draw the square
+ GLES20.glDrawElements(
+ GLES20.GL_TRIANGLES, DRAW_ORDER.size,
+ GLES20.GL_UNSIGNED_SHORT, mDrawListBuffer
+ )
+ // Disable vertex array
+ GLES20.glDisableVertexAttribArray(mPositionHandle)
+ }
+
+ companion object {
+ private val vertexShaderCode =
+ """
+ uniform mat4 uMVPMatrix;
+ attribute vec4 vPosition;
+ void main() {
+ gl_Position = uMVPMatrix * vPosition;
+ }
+ """
+ private val fragmentShaderCode =
+ """
+ precision mediump float;
+ uniform vec4 vColor;
+ void main() {
+ gl_FragColor = vColor;
+ }
+ """
+
+ // number of coordinates per vertex in this array
+ val COORDS_PER_VERTEX = 3
+ val DRAW_ORDER = shortArrayOf(0, 1, 2, 0, 2, 3) // order to draw vertices
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
index 03eec57..313ac2d 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/opengl/GLRendererTest.kt
@@ -40,10 +40,10 @@
import androidx.graphics.lowlatency.FrameBufferRenderer
import androidx.graphics.lowlatency.FrameBuffer
import androidx.graphics.lowlatency.LineRenderer
+import androidx.graphics.lowlatency.Rectangle
import androidx.graphics.lowlatency.SyncFenceCompat
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
-import androidx.graphics.opengl.egl.deviceSupportsNativeAndroidFence
import androidx.graphics.opengl.egl.supportsNativeAndroidFence
import androidx.lifecycle.Lifecycle.State
import androidx.test.core.app.ActivityScenario
@@ -800,13 +800,6 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testFrontBufferedRenderer() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding
- // EGL Extensions to obtain native Android fence objects from EGLSync
- // instances then skip this test as we cannot guarantee consistency
- // for front buffered rendering
- return
- }
val width = 10
val height = 10
val renderLatch = CountDownLatch(1)
@@ -814,8 +807,12 @@
val glRenderer = GLRenderer().apply { start() }
var frameBuffer: FrameBuffer? = null
var status: Boolean? = false
+ var supportsFence = false
val callbacks = object : FrameBufferRenderer.RenderCallback {
+
+ private val mOrthoMatrix = FloatArray(16)
+
override fun obtainFrameBuffer(egl: EGLSpec): FrameBuffer =
FrameBuffer(
egl,
@@ -829,9 +826,26 @@
).also { frameBuffer = it }
override fun onDraw(eglManager: EGLManager) {
- GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
- GLES20.glFlush()
+ GLES20.glViewport(0, 0, width, height)
+ Matrix.orthoM(
+ mOrthoMatrix,
+ 0,
+ 0f,
+ width.toFloat(),
+ 0f,
+ height.toFloat(),
+ -1f,
+ 1f
+ )
+ Rectangle().draw(
+ mOrthoMatrix,
+ Color.RED,
+ 0f,
+ 0f,
+ width.toFloat(),
+ height.toFloat()
+ )
+ supportsFence = eglManager.supportsNativeAndroidFence()
}
override fun onDrawComplete(
@@ -852,9 +866,11 @@
var hardwareBuffer: HardwareBuffer? = null
try {
assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
- assert(status != null)
- status?.let {
- assertTrue(it)
+ if (supportsFence) {
+ assert(status != null)
+ status?.let {
+ assertTrue(it)
+ }
}
hardwareBuffer = frameBuffer?.hardwareBuffer
@@ -883,13 +899,6 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
fun testFrameBufferRendererWithSyncFence() {
- if (!deviceSupportsNativeAndroidFence()) {
- // If the Android device does not support the corresponding
- // EGL Extensions to obtain native Android fence objects from EGLSync
- // instances then skip this test as we cannot guarantee consistency
- // for front buffered rendering
- return
- }
val width = 10
val height = 10
@@ -900,6 +909,7 @@
var startTime = Long.MAX_VALUE
var signalTime = 0L
+ var supportsFence = false
val renderer =
object : FrameBufferRenderer.RenderCallback, GLRenderer.EGLContextCallback {
private val mMVPMatrix = FloatArray(16)
@@ -947,6 +957,8 @@
mLines[3] = 5f
mLineRenderer.drawLines(mMVPMatrix, mLines)
assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
+
+ supportsFence = eglManager.supportsNativeAndroidFence()
}
@WorkerThread
@@ -954,17 +966,18 @@
frameBuffer: FrameBuffer,
syncFenceCompat: SyncFenceCompat?
) {
- assertNotNull(syncFenceCompat)
- assertTrue(syncFenceCompat!!.isValid())
+ if (supportsFence) {
+ assertNotNull(syncFenceCompat)
+ assertTrue(syncFenceCompat!!.isValid())
+ assertTrue(syncFenceCompat.await(3000))
+ signalTime = syncFenceCompat.getSignalTimeNanos()
+
+ assertTrue(syncFenceCompat.getSignalTimeNanos() < System.nanoTime())
+ assertTrue(syncFenceCompat.getSignalTimeNanos() > startTime)
+ }
+ renderLatch.countDown()
assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
-
- assertTrue(syncFenceCompat.await(3000))
- signalTime = syncFenceCompat.getSignalTimeNanos()
-
- renderLatch.countDown()
- assertTrue(syncFenceCompat.getSignalTimeNanos() < System.nanoTime())
- assertTrue(syncFenceCompat.getSignalTimeNanos() > startTime)
}
}
@@ -978,8 +991,10 @@
try {
assertTrue(renderLatch.await(3000, TimeUnit.MILLISECONDS))
- assertTrue(startTime < signalTime)
- assertTrue(signalTime < System.nanoTime())
+ if (supportsFence) {
+ assertTrue(startTime < signalTime)
+ assertTrue(signalTime < System.nanoTime())
+ }
} finally {
glRenderer.stop(true) {
teardownLatch.countDown()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
index f5b3c2a..bf68dd4 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/FrameBufferRenderer.kt
@@ -28,6 +28,7 @@
import androidx.graphics.opengl.GLRenderer
import androidx.graphics.opengl.egl.EGLManager
import androidx.graphics.opengl.egl.EGLSpec
+import androidx.opengl.EGLExt
import java.util.concurrent.atomic.AtomicBoolean
import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
@@ -71,16 +72,40 @@
syncFenceCompat = if (eglManager.supportsNativeAndroidFence()) {
syncStrategy.createSyncFence(egl)
+ } else if (eglManager.isExtensionSupported(EGL_KHR_FENCE_SYNC)) {
+ // In this case the device only supports EGL sync objects but not creation
+ // of native SyncFence objects from an EGLSync.
+ // This usually occurs in emulator/cuttlefish instances as well as ChromeOS devices
+ // running ARC++. In this case fallback onto creating a sync object and waiting
+ // on it instead.
+ // TODO b/256217036 block on another thread instead of waiting here
+ val syncKhr = egl.eglCreateSyncKHR(EGLExt.EGL_SYNC_FENCE_KHR, null)
+ if (syncKhr != null) {
+ GLES20.glFlush()
+ val status = egl.eglClientWaitSyncKHR(
+ syncKhr,
+ EGLExt.EGL_SYNC_FLUSH_COMMANDS_BIT_KHR,
+ EGLExt.EGL_FOREVER_KHR
+ )
+ if (status != EGLExt.EGL_CONDITION_SATISFIED_KHR) {
+ Log.w(TAG, "warning waiting on sync object: $status")
+ }
+ } else {
+ Log.w(TAG, "Unable to create EGLSync")
+ GLES20.glFinish()
+ }
+ null
} else {
- Log.w(TAG, "Device does not support creation of native fences")
+ Log.w(TAG, "Device does not support creation of any fences")
+ GLES20.glFinish()
null
}
-
+ } catch (exception: Exception) {
+ Log.w(TAG, "Error attempting to render to frame buffer: ${exception.message}")
+ } finally {
// At this point the HardwareBuffer has the contents of the GL rendering
// Create a surface Control transaction to dispatch this request
frameBufferRendererCallbacks.onDrawComplete(buffer, syncFenceCompat)
- } finally {
- syncFenceCompat?.close()
}
}
@@ -201,6 +226,7 @@
return if (!isVisible) {
eglSpec.createNativeSyncFence()
} else if (supportsFrontBufferUsage) {
+ GLES20.glFlush()
return null
} else {
val fence = eglSpec.createNativeSyncFence()
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
index 1e64338d..1001bdb 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/GLFrontBufferedRenderer.kt
@@ -31,8 +31,10 @@
import androidx.graphics.surface.SurfaceControlCompat
import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
-import java.util.Collections
+import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
+import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executors
/**
* Class responsible for supporting a "front buffered" rendering system. This allows for lower
@@ -81,6 +83,11 @@
frontBufferedLayerSurfaceControl: SurfaceControlCompat,
transaction: SurfaceControlCompat.Transaction
) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ transaction.addTransactionCommittedListener(mExecutor, mCommittedListener)
+ } else {
+ clearFrontBuffer()
+ }
mFrontBufferSyncStrategy.isVisible = false
callback.onDoubleBufferedLayerRenderComplete(
frontBufferedLayerSurfaceControl,
@@ -101,6 +108,19 @@
}
}
+ private val mExecutor = Executors.newSingleThreadExecutor()
+
+ private val mCommittedListener = object : SurfaceControlCompat.TransactionCommittedListener {
+ override fun onTransactionCommitted() {
+ clearFrontBuffer()
+ }
+ }
+
+ internal fun clearFrontBuffer() {
+ mFrontBufferedLayerRenderer?.clear()
+ mFrontBufferedRenderTarget?.requestRender()
+ }
+
/**
* [GLRenderer.EGLContextCallback]s used to release the corresponding [FrameBufferPool]
* if the [GLRenderer] is torn down.
@@ -110,7 +130,12 @@
*/
private val mContextCallbacks = object : GLRenderer.EGLContextCallback {
override fun onEGLContextCreated(eglManager: EGLManager) {
- // no-op
+ with(eglManager) {
+ val supportsEglFences = isExtensionSupported(EGL_KHR_FENCE_SYNC)
+ val supportsAndroidFences = isExtensionSupported(EGL_ANDROID_NATIVE_FENCE_SYNC)
+ Log.d(TAG, "Supports KHR_FENCE_SYNC: $supportsEglFences")
+ Log.d(TAG, "Supports ANDROID_NATIVE_FENCE_SYNC: $supportsAndroidFences")
+ }
}
override fun onEGLContextDestroyed(eglManager: EGLManager) {
@@ -130,11 +155,11 @@
}
override fun onLayerDestroyed() {
- release(true)
+ detachTargets(true)
}
- override fun obtainDoubleBufferedLayerParams(): MutableCollection<T> =
- mParentBufferParamQueue
+ override fun obtainDoubleBufferedLayerParams(): MutableCollection<T>? =
+ mSegments.poll()
override fun getFrontBufferedLayerSurfaceControl(): SurfaceControlCompat? =
mFrontBufferedLayerSurfaceControl
@@ -147,7 +172,7 @@
* Queue of parameters to be consumed in [Callback.onDrawFrontBufferedLayer] with the parameter
* provided in [renderFrontBufferedLayer]
*/
- private val mFrontBufferQueueParams = ConcurrentLinkedQueue<T>()
+ private val mActiveSegment = ParamQueue<T>()
/**
* Collection of parameters to be consumed in [Callback.onDoubleBufferedLayerRenderComplete]
@@ -156,7 +181,7 @@
* this collection is cleared and new parameters are added to it with consecutive calls to
* [renderFrontBufferedLayer].
*/
- private val mParentBufferParamQueue = Collections.synchronizedList(ArrayList<T>())
+ private val mSegments = ConcurrentLinkedQueue<MutableCollection<T>>()
/**
* [FrameBuffer] used for rendering into the front buffered layer. This buffer is persisted
@@ -257,8 +282,6 @@
}
renderer.registerEGLContextCallback(mContextCallbacks)
- mDoubleBufferedLayerRenderTarget =
- mParentRenderLayer.createRenderTarget(renderer, mCallback)
mGLRenderer = renderer
mHardwareBufferUsageFlags = obtainHardwareBufferUsageFlags()
@@ -267,7 +290,11 @@
}
internal fun update(width: Int, height: Int) {
- if (mWidth != width || mHeight != height) {
+ if (mWidth != width || mHeight != height && isValid()) {
+
+ mDoubleBufferedLayerRenderTarget?.detach(true)
+ val doubleBufferTarget = mParentRenderLayer.createRenderTarget(mGLRenderer, mCallback)
+
mFrontBufferedLayerSurfaceControl?.release()
val frontBufferedSurfaceControl = SurfaceControlCompat.Builder()
@@ -326,6 +353,7 @@
mFrontBufferedLayerRenderer = frontBufferedLayerRenderer
mFrontBufferedLayerSurfaceControl = frontBufferedSurfaceControl
+ mDoubleBufferedLayerRenderTarget = doubleBufferTarget
mBufferPool = bufferPool
mWidth = width
mHeight = height
@@ -357,8 +385,7 @@
*/
fun renderFrontBufferedLayer(param: T) {
if (isValid()) {
- mFrontBufferQueueParams.add(param)
- mParentBufferParamQueue.add(param)
+ mActiveSegment.add(param)
mFrontBufferedRenderTarget?.requestRender()
} else {
Log.w(
@@ -389,9 +416,8 @@
*/
fun commit() {
if (isValid()) {
- mFrontBufferQueueParams.clear()
+ mSegments.add(mActiveSegment.release())
mDoubleBufferedLayerRenderTarget?.requestRender()
- mFrontBufferedLayerRenderer?.clear()
} else {
Log.w(
TAG, "Attempt to render to the double buffered layer when " +
@@ -401,24 +427,10 @@
}
/**
- * Releases the [GLFrontBufferedRenderer] and provides an optional callback that is invoked when
- * the [GLFrontBufferedRenderer] is fully torn down. If the [cancelPending] flag is true, all
- * pending requests to render into the front or double buffered layers will be processed before
- * the [GLFrontBufferedRenderer] is torn down. Otherwise all in process requests are ignored.
- * If the [GLFrontBufferedRenderer] is already released, that is [isValid] returns `false`, this
- * method does nothing.
- *
- * @param cancelPending Flag indicating that requests to render should be processed before
- * the [GLFrontBufferedRenderer] is released
- * @param onReleaseComplete Optional callback invoked when the [GLFrontBufferedRenderer] has
- * been released. This callback is invoked on the backing GLThread
+ * Helper method used to detach the front and multi buffered render targets as well as
+ * release SurfaceControl instances
*/
- @JvmOverloads
- fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
- if (!isValid()) {
- Log.w(TAG, "Attempt to release GLFrontbufferedRenderer that is already released")
- return
- }
+ internal fun detachTargets(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
// Wrap the callback into a separate lambda to ensure it is invoked only after
// both the front and double buffered layer target renderers are detached
var callbackCount = 0
@@ -448,10 +460,35 @@
onReleaseComplete?.invoke()
}
}
+ mFrontBufferedLayerSurfaceControl = null
mFrontBufferedRenderTarget?.detach(cancelPending, wrappedCallback)
mDoubleBufferedLayerRenderTarget?.detach(cancelPending, wrappedCallback)
mFrontBufferedRenderTarget = null
mDoubleBufferedLayerRenderTarget = null
+ mWidth = -1
+ mHeight = -1
+ }
+
+ /**
+ * Releases the [GLFrontBufferedRenderer] and provides an optional callback that is invoked when
+ * the [GLFrontBufferedRenderer] is fully torn down. If the [cancelPending] flag is true, all
+ * pending requests to render into the front or double buffered layers will be processed before
+ * the [GLFrontBufferedRenderer] is torn down. Otherwise all in process requests are ignored.
+ * If the [GLFrontBufferedRenderer] is already released, that is [isValid] returns `false`, this
+ * method does nothing.
+ *
+ * @param cancelPending Flag indicating that requests to render should be processed before
+ * the [GLFrontBufferedRenderer] is released
+ * @param onReleaseComplete Optional callback invoked when the [GLFrontBufferedRenderer] has
+ * been released. This callback is invoked on the backing GLThread
+ */
+ @JvmOverloads
+ fun release(cancelPending: Boolean, onReleaseComplete: (() -> Unit)? = null) {
+ if (!isValid()) {
+ Log.w(TAG, "Attempt to release GLFrontbufferedRenderer that is already released")
+ return
+ }
+ detachTargets(cancelPending, onReleaseComplete)
mGLRenderer.unregisterEGLContextCallback(mContextCallbacks)
if (mIsManagingGLRenderer) {
@@ -463,8 +500,7 @@
mGLRenderer.stop(false)
}
- mFrontBufferedLayerSurfaceControl = null
- mParentRenderLayer.setParentLayerCallbacks(null)
+ mExecutor.shutdown()
mIsReleased = true
}
@@ -508,24 +544,14 @@
@WorkerThread
override fun onDraw(eglManager: EGLManager) {
- try {
- // Explicitly call remove in order to delineate between scenarios where
- // no parameters are provided and the consumer explicitly supports nullable
- // parameters.
- // If poll was used instead, we would not be able to determine if the nullable
- // parameter was because there were no items in the queue or the consumer
- // explicitly provided null as a placeholder
+ mActiveSegment.next { param ->
mCallback.onDrawFrontBufferedLayer(
eglManager,
mBufferTransform.glWidth,
mBufferTransform.glHeight,
mBufferTransform.transform,
- mFrontBufferQueueParams.remove()
+ param
)
- } catch (_: NoSuchElementException) {
- // Skip rendering if we have been told to render but we do not have parameters
- // Because the call to render to the front buffer takes in a parameter we should
- // not run into this scenario.
}
}
@@ -564,8 +590,8 @@
}
private fun clearParamQueues() {
- mFrontBufferQueueParams.clear()
- mParentBufferParamQueue.clear()
+ mActiveSegment.clear()
+ mSegments.clear()
}
/**
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
new file mode 100644
index 0000000..a0b1765
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParamQueue.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.graphics.lowlatency
+
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
+
+/**
+ * Thread-safe class responsible for maintaining a list of objects as well as an index
+ * that keeps track of the last consumed object within the queue.
+ * This ensures that the collection as well as the current index parameter are updated atomically
+ * within the same lock.
+ */
+internal class ParamQueue<T> {
+
+ private val mLock = ReentrantLock()
+ private var mParams = ArrayList<T>()
+ private var mIndex = 0
+
+ /**
+ * Clears the parameter queue and resets the index position to 0
+ */
+ fun clear() {
+ mLock.withLock {
+ mIndex = 0
+ mParams.clear()
+ }
+ }
+
+ /**
+ * Returns the current queue and resets the index position to 0.
+ * This collection is no longer owned by [ParamQueue] and a new queue is maintained after
+ * this call is made. This allows callers to manipulate the returned collection as they
+ * see fit without impacting the integrity of the [ParamQueue] after this method is invoked.
+ */
+ fun release(): MutableCollection<T> {
+ mLock.withLock {
+ val result = mParams
+ mParams = ArrayList<T>()
+ mIndex = 0
+ return result
+ }
+ }
+
+ /**
+ * Returns the next parameter at the index position and increments the index position.
+ * This parameter is provided as a callback. If the index position is out of range, this
+ * method acts as a no-op.
+ * This does not actually remove the parameter from the collection. Consumers must either
+ * call [clear], or clear the collection returned in [release] to ensure contents do not
+ * grow unbounded.
+ */
+ inline fun next(block: (T) -> Unit) {
+ mLock.withLock {
+ if (mIndex < mParams.size) {
+ val param = mParams[mIndex++]
+ block(param)
+ }
+ }
+ }
+
+ /**
+ * Adds a new entry into the parameter queue. This leaves the current index position unchanged.
+ */
+ fun add(param: T) {
+ mLock.withLock {
+ mParams.add(param)
+ }
+ }
+}
\ No newline at end of file
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index 20e4d7b..b217dd9 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -104,12 +104,14 @@
/**
* Callback invoked by the [ParentRenderLayer] to query the parameters since the last
- * render to the dry layer. This includes all parameters to each request to render content
- * to the front buffered layer since the last time the dry layer was re-rendered.
+ * render to the multi-buffered layer. This includes all parameters to each request to
+ * render content to the front buffered layer since the last time the dry layer was
+ * re-rendered.
* This is useful for recreating the entire scene when front buffered layer contents are to
* be committed, that is the entire scene is re-rendered into the double buffered layer.
+ * This can return null if all the double buffered params have already been queried.
*/
- fun obtainDoubleBufferedLayerParams(): MutableCollection<T>
+ fun obtainDoubleBufferedLayerParams(): MutableCollection<T>?
/**
* Obtain a handle to the front buffered layer [SurfaceControlCompat] to be used in
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index 4ce82fe..c31a83f 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -45,6 +45,35 @@
private val mTransformResolver = BufferTransformHintResolver()
+ private var transformHint = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+ private var inverse = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+ init {
+ surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
+
+ override fun surfaceCreated(holder: SurfaceHolder) {
+ // NO-OP wait on surfaceChanged callback
+ }
+
+ override fun surfaceChanged(
+ holder: SurfaceHolder,
+ format: Int,
+ width: Int,
+ height: Int
+ ) {
+ transformHint = getBufferTransformHint()
+ inverse = mBufferTransform.invertBufferTransform(transformHint)
+ mBufferTransform.computeTransform(width, height, inverse)
+ mParentSurfaceControl?.release()
+ mLayerCallback?.onSizeChanged(width, height)
+ mParentSurfaceControl = createDoubleBufferedSurfaceControl()
+ }
+
+ override fun surfaceDestroyed(p0: SurfaceHolder) {
+ mLayerCallback?.onLayerDestroyed()
+ }
+ })
+ }
+
override fun getBufferTransformHint(): Int {
return mTransformResolver.getBufferTransformHint(surfaceView)
}
@@ -65,8 +94,7 @@
renderer: GLRenderer,
renderLayerCallback: GLFrontBufferedRenderer.Callback<T>
): GLRenderer.RenderTarget {
- var transformHint = BufferTransformHintResolver.UNKNOWN_TRANSFORM
- var inverse = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+ var params: Collection<T>? = null
val frameBufferRenderer = FrameBufferRenderer(
object : FrameBufferRenderer.RenderCallback {
@@ -75,7 +103,6 @@
?: throw IllegalArgumentException("No FrameBufferPool available")
override fun onDraw(eglManager: EGLManager) {
- val params = mLayerCallback?.obtainDoubleBufferedLayerParams()
renderLayerCallback.onDrawDoubleBufferedLayer(
eglManager,
mBufferTransform.glWidth,
@@ -125,41 +152,25 @@
}
}
})
- surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
-
- override fun surfaceCreated(holder: SurfaceHolder) {
- // NO-OP wait on surfaceChanged callback
- }
-
- override fun surfaceChanged(
- holder: SurfaceHolder,
- format: Int,
- width: Int,
- height: Int
- ) {
- transformHint = getBufferTransformHint()
- inverse = mBufferTransform.invertBufferTransform(transformHint)
- mBufferTransform.computeTransform(width, height, inverse)
- mParentSurfaceControl?.release()
- mLayerCallback?.onSizeChanged(width, height)
- mParentSurfaceControl = createDoubleBufferedSurfaceControl()
- }
-
- override fun surfaceDestroyed(p0: SurfaceHolder) {
- mLayerCallback?.onLayerDestroyed()
- }
- })
- val renderTarget = renderer.attach(surfaceView, frameBufferRenderer)
+ val parentFrameBufferRenderer = WrapperFrameBufferRenderer<T>(frameBufferRenderer) {
+ params = mLayerCallback?.obtainDoubleBufferedLayerParams()
+ params != null
+ }
+ val renderTarget = renderer.attach(surfaceView, parentFrameBufferRenderer)
mRenderTarget = renderTarget
mFrameBufferRenderer = frameBufferRenderer
return renderTarget
}
- internal fun createDoubleBufferedSurfaceControl(): SurfaceControlCompat =
- SurfaceControlCompat.Builder()
+ internal fun createDoubleBufferedSurfaceControl(): SurfaceControlCompat {
+ val surfaceControl = SurfaceControlCompat.Builder()
.setParent(surfaceView)
.setName("DoubleBufferedLayer")
.build()
+ // SurfaceControl is not visible by default so make it visible right after creation
+ SurfaceControlCompat.Transaction().setVisibility(surfaceControl, true).commit()
+ return surfaceControl
+ }
override fun setParentLayerCallbacks(callback: ParentRenderLayer.Callback<T>?) {
mLayerCallback = callback
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
new file mode 100644
index 0000000..005b06d
--- /dev/null
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/WrapperFrameBufferRenderer.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.graphics.lowlatency
+
+import android.opengl.EGLConfig
+import android.opengl.EGLSurface
+import android.os.Build
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.graphics.opengl.GLRenderer
+import androidx.graphics.opengl.egl.EGLManager
+import androidx.graphics.opengl.egl.EGLSpec
+
+/**
+ * Wrapper Renderer around [FrameBufferRenderer] that skips rendering on a given condition
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+internal class WrapperFrameBufferRenderer<T>(
+ private val frameBufferRenderer: FrameBufferRenderer,
+ private val shouldRender: () -> Boolean
+) : GLRenderer.RenderCallback {
+
+ override fun onSurfaceCreated(
+ spec: EGLSpec,
+ config: EGLConfig,
+ surface: Surface,
+ width: Int,
+ height: Int
+ ): EGLSurface? = frameBufferRenderer.onSurfaceCreated(spec, config, surface, width, height)
+
+ override fun onDrawFrame(eglManager: EGLManager) {
+ if (shouldRender()) {
+ frameBufferRenderer.onDrawFrame(eglManager)
+ }
+ }
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
index a3806cb..97541f7 100644
--- a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
@@ -51,7 +51,7 @@
* API version of the AIDL interface. Should be incremented every time a new
* method is added.
*/
- const int CURRENT_API_VERSION = 4;
+ const int CURRENT_API_VERSION = 5;
const int MIN_API_VERSION = 1;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index b3a6483..e80113c 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -16,7 +16,58 @@
package androidx.health.connect.client.permission
import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseEventRecord
+import androidx.health.connect.client.records.ExerciseLapRecord
+import androidx.health.connect.client.records.ExerciseRepetitionsRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
+import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
+import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HipCircumferenceRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SleepStageRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.SwimmingStrokesRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WaistCircumferenceRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
import kotlin.reflect.KClass
/**
@@ -44,16 +95,52 @@
}
/**
+ * Returns a permission defined in [HealthPermission] to read provided [recordType], such as
+ * `Steps::class`.
+ *
+ * @return Permission to use with [androidx.health.connect.client.PermissionController].
+ * @throws IllegalArgumentException if the given record type is invalid.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+ @JvmStatic
+ public fun createReadPermissionInternal(recordType: KClass<out Record>): String {
+ if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
+ throw IllegalArgumentException(
+ "Given recordType is not valid : $recordType.simpleName"
+ )
+ }
+ return READ_PERMISSION_PREFIX + RECORD_TYPE_TO_PERMISSION[recordType]
+ }
+
+ /**
* Creates [HealthPermission] to write provided [recordType], such as `Steps::class`.
*
- * @return Permission object to use with
- * [androidx.health.connect.client.PermissionController].
+ * @return Permission to use with [androidx.health.connect.client.PermissionController].
*/
@JvmStatic
public fun createWritePermission(recordType: KClass<out Record>): HealthPermission {
return HealthPermission(recordType, AccessTypes.WRITE)
}
+ /**
+ * Returns a permission defined in [HealthPermission] to read provided [recordType], such as
+ * `Steps::class`.
+ *
+ * @return Permission object to use with
+ * [androidx.health.connect.client.PermissionController].
+ * @throws IllegalArgumentException if the given record type is invalid.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+ @JvmStatic
+ public fun createWritePermissionInternal(recordType: KClass<out Record>): String {
+ if (RECORD_TYPE_TO_PERMISSION[recordType] == null) {
+ throw IllegalArgumentException(
+ "Given recordType is not valid : $recordType.simpleName"
+ )
+ }
+ return WRITE_PERMISSION_PREFIX + RECORD_TYPE_TO_PERMISSION.getOrDefault(recordType, "")
+ }
+
// Read permissions for ACTIVITY.
@RestrictTo(RestrictTo.Scope.LIBRARY)
const val READ_ACTIVE_CALORIES_BURNED =
@@ -230,6 +317,99 @@
const val WRITE_RESPIRATORY_RATE = "android.permission.health.WRITE_RESPIRATORY_RATE"
@RestrictTo(RestrictTo.Scope.LIBRARY)
const val WRITE_RESTING_HEART_RATE = "android.permission.health.WRITE_RESTING_HEART_RATE"
+
+ private const val READ_PERMISSION_PREFIX = "android.permission.health.READ_"
+
+ private const val WRITE_PERMISSION_PREFIX = "android.permission.health.WRITE_"
+
+ private val RECORD_TYPE_TO_PERMISSION =
+ mapOf<KClass<out Record>, String>(
+ ActiveCaloriesBurnedRecord::class to
+ READ_ACTIVE_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
+ BasalBodyTemperatureRecord::class to
+ READ_BASAL_BODY_TEMPERATURE.substringAfter(READ_PERMISSION_PREFIX),
+ BasalMetabolicRateRecord::class to
+ READ_BASAL_METABOLIC_RATE.substringAfter(READ_PERMISSION_PREFIX),
+ BloodGlucoseRecord::class to
+ READ_BLOOD_GLUCOSE.substringAfter(READ_PERMISSION_PREFIX),
+ BloodPressureRecord::class to
+ READ_BLOOD_PRESSURE.substringAfter(READ_PERMISSION_PREFIX),
+ BodyFatRecord::class to READ_BODY_FAT.substringAfter(READ_PERMISSION_PREFIX),
+ BodyTemperatureRecord::class to
+ READ_BODY_TEMPERATURE.substringAfter(READ_PERMISSION_PREFIX),
+ BodyWaterMassRecord::class to
+ READ_BODY_WATER_MASS.substringAfter(READ_PERMISSION_PREFIX),
+ BoneMassRecord::class to READ_BONE_MASS.substringAfter(READ_PERMISSION_PREFIX),
+ CervicalMucusRecord::class to
+ READ_CERVICAL_MUCUS.substringAfter(READ_PERMISSION_PREFIX),
+ CyclingPedalingCadenceRecord::class to
+ READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ DistanceRecord::class to READ_DISTANCE.substringAfter(READ_PERMISSION_PREFIX),
+ ElevationGainedRecord::class to
+ READ_ELEVATION_GAINED.substringAfter(READ_PERMISSION_PREFIX),
+ ExerciseEventRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ ExerciseLapRecord::class to READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ ExerciseRepetitionsRecord::class to
+ READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ ExerciseSessionRecord::class to
+ READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ FloorsClimbedRecord::class to
+ READ_FLOORS_CLIMBED.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateRecord::class to READ_HEART_RATE.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilityDifferentialIndexRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilityRmssdRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySd2Record::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySdannRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySdnnIndexRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySdnnRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySdsdRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilitySRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeartRateVariabilityTinnRecord::class to
+ READ_HEART_RATE_VARIABILITY.substringAfter(READ_PERMISSION_PREFIX),
+ HeightRecord::class to READ_HEIGHT.substringAfter(READ_PERMISSION_PREFIX),
+ HipCircumferenceRecord::class to
+ READ_HIP_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
+ HydrationRecord::class to READ_HYDRATION.substringAfter(READ_PERMISSION_PREFIX),
+ LeanBodyMassRecord::class to
+ READ_LEAN_BODY_MASS.substringAfter(READ_PERMISSION_PREFIX),
+ MenstruationFlowRecord::class to
+ READ_MENSTRUATION.substringAfter(READ_PERMISSION_PREFIX),
+ NutritionRecord::class to READ_NUTRITION.substringAfter(READ_PERMISSION_PREFIX),
+ OvulationTestRecord::class to
+ READ_OVULATION_TEST.substringAfter(READ_PERMISSION_PREFIX),
+ OxygenSaturationRecord::class to
+ READ_OXYGEN_SATURATION.substringAfter(READ_PERMISSION_PREFIX),
+ PowerRecord::class to READ_POWER.substringAfter(READ_PERMISSION_PREFIX),
+ RespiratoryRateRecord::class to
+ READ_RESPIRATORY_RATE.substringAfter(READ_PERMISSION_PREFIX),
+ RestingHeartRateRecord::class to
+ READ_RESTING_HEART_RATE.substringAfter(READ_PERMISSION_PREFIX),
+ SexualActivityRecord::class to
+ READ_SEXUAL_ACTIVITY.substringAfter(READ_PERMISSION_PREFIX),
+ SleepSessionRecord::class to READ_SLEEP.substringAfter(READ_PERMISSION_PREFIX),
+ SleepStageRecord::class to READ_SLEEP.substringAfter(READ_PERMISSION_PREFIX),
+ SpeedRecord::class to READ_SPEED.substringAfter(READ_PERMISSION_PREFIX),
+ StepsCadenceRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
+ StepsRecord::class to READ_STEPS.substringAfter(READ_PERMISSION_PREFIX),
+ SwimmingStrokesRecord::class to
+ READ_EXERCISE.substringAfter(READ_PERMISSION_PREFIX),
+ TotalCaloriesBurnedRecord::class to
+ READ_TOTAL_CALORIES_BURNED.substringAfter(READ_PERMISSION_PREFIX),
+ Vo2MaxRecord::class to READ_VO2_MAX.substringAfter(READ_PERMISSION_PREFIX),
+ WaistCircumferenceRecord::class to
+ READ_WAIST_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
+ WeightRecord::class to READ_WEIGHT.substringAfter(READ_PERMISSION_PREFIX),
+ WheelchairPushesRecord::class to
+ READ_WHEELCHAIR_PUSHES.substringAfter(READ_PERMISSION_PREFIX),
+ )
}
override fun equals(other: Any?): Boolean {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
index d2e6388..1a566d9 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SleepStageRecord.kt
@@ -83,6 +83,7 @@
"light" to STAGE_TYPE_LIGHT,
"deep" to STAGE_TYPE_DEEP,
"rem" to STAGE_TYPE_REM,
+ "unknown" to STAGE_TYPE_UNKNOWN
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
index 599b99d..fe4f595 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
@@ -93,7 +93,7 @@
override fun filterGrantedPermissions(
permissions: Set<PermissionProto.Permission>,
): ListenableFuture<Set<PermissionProto.Permission>> {
- return executeWithVersionCheck(min(MIN_API_VERSION, 4)) { service, resultFuture ->
+ return executeWithVersionCheck(min(MIN_API_VERSION, 5)) { service, resultFuture ->
service.filterGrantedPermissions(
getRequestContext(),
permissions.map { Permission(it) }.toList(),
@@ -207,7 +207,7 @@
override fun registerForDataNotifications(
request: RequestProto.RegisterForDataNotificationsRequest,
): ListenableFuture<Void> =
- executeWithVersionCheck(/* minApiVersion = */ 2) { service, resultFuture ->
+ executeWithVersionCheck(min(MIN_API_VERSION, 2)) { service, resultFuture ->
service.registerForDataNotifications(
getRequestContext(),
RegisterForDataNotificationsRequest(request),
@@ -218,7 +218,7 @@
override fun unregisterFromDataNotifications(
request: RequestProto.UnregisterFromDataNotificationsRequest,
): ListenableFuture<Void> =
- executeWithVersionCheck(/* minApiVersion = */ 2) { service, resultFuture ->
+ executeWithVersionCheck(min(MIN_API_VERSION, 2)) { service, resultFuture ->
service.unregisterFromDataNotifications(
getRequestContext(),
UnregisterFromDataNotificationsRequest(request),
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
index 5ffb521..5539e49 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
@@ -16,16 +16,19 @@
package androidx.health.connect.client
+import androidx.health.connect.client.records.Record
import java.io.File
import java.net.URL
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.reflect.KClass
-val RECORD_CLASSES: List<KClass<*>> by lazy {
+@Suppress("UNCHECKED_CAST")
+val RECORD_CLASSES: List<KClass<out Record>> by lazy {
findClasses("androidx.health.connect.client.records")
.filterNot { it.java.isInterface }
.filter { it.simpleName.orEmpty().endsWith("Record") }
+ .map { it as KClass<out Record> }
}
fun findClasses(packageName: String): Set<KClass<*>> {
@@ -43,20 +46,19 @@
}
}
-private fun findClasses(directory: String, packageName: String): Set<String> =
- buildSet {
- if (directory.startsWith("file:") && ('!' in directory)) {
- addAll(unzipClasses(path = directory, packageName = packageName))
- }
+private fun findClasses(directory: String, packageName: String): Set<String> = buildSet {
+ if (directory.startsWith("file:") && ('!' in directory)) {
+ addAll(unzipClasses(path = directory, packageName = packageName))
+ }
- for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
- if (file.isDirectory) {
- addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
- } else if (file.name.endsWith(".class")) {
- add("$packageName.${file.name.dropLast(6)}")
- }
+ for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
+ if (file.isDirectory) {
+ addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
+ } else if (file.name.endsWith(".class")) {
+ add("$packageName.${file.name.dropLast(6)}")
}
}
+}
private fun unzipClasses(path: String, packageName: String): Set<String> =
ZipInputStream(URL(path.substringBefore('!')).openStream()).use { zip ->
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
index 7dc78ba..fd39a8f 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/PermissionControllerTest.kt
@@ -51,4 +51,20 @@
Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
Truth.assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
}
+
+ @Test
+ fun createIntentTest_permissionStrings() {
+ val requestPermissionContract =
+ PermissionController.createRequestPermissionResultContractInternal(
+ PROVIDER_PACKAGE_NAME
+ )
+ val intent =
+ requestPermissionContract.createIntent(
+ context,
+ setOf(HealthPermission.READ_ACTIVE_CALORIES_BURNED)
+ )
+
+ Truth.assertThat(intent.action).isEqualTo("androidx.health.ACTION_REQUEST_PERMISSIONS")
+ Truth.assertThat(intent.`package`).isEqualTo(PROVIDER_PACKAGE_NAME)
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt
new file mode 100644
index 0000000..cd0e471
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/SleepStageConverterTest.kt
@@ -0,0 +1,38 @@
+package androidx.health.connect.client.impl.converters.records
+
+import androidx.health.connect.client.records.SleepStageRecord
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.time.ZoneOffset
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val START_TIME = Instant.ofEpochMilli(1234L)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val END_TIME = Instant.ofEpochMilli(5678L)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val START_ZONE_OFFSET = ZoneOffset.ofHours(1)
+@SuppressWarnings("GoodTime") // Safe to use in test
+private val END_ZONE_OFFSET = ZoneOffset.ofHours(2)
+
+@RunWith(JUnit4::class)
+class SleepStageConverterTest {
+
+ // Because the APK schema has stage type required
+ // we will need to serialize "unknown" explicitly.
+ @Test
+ fun unknownStageType_serializesUnknown() {
+ val record =
+ SleepStageRecord(
+ stage = SleepStageRecord.STAGE_TYPE_UNKNOWN,
+ startTime = START_TIME,
+ startZoneOffset = START_ZONE_OFFSET,
+ endTime = END_TIME,
+ endZoneOffset = END_ZONE_OFFSET,
+ )
+ val proto = record.toProto()
+ assertThat(proto.valuesMap).containsExactly("stage", enumVal("unknown"))
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
index 7d5a4c3..eb2649b 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/permission/HealthPermissionTest.kt
@@ -15,9 +15,13 @@
*/
package androidx.health.connect.client.permission
+import androidx.health.connect.client.RECORD_CLASSES
+import androidx.health.connect.client.records.ExerciseRouteRecord
+import androidx.health.connect.client.records.Record
import androidx.health.connect.client.records.StepsRecord
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,4 +41,48 @@
assertThat(permission.accessType).isEqualTo(AccessTypes.WRITE)
assertThat(permission.recordType).isEqualTo(StepsRecord::class)
}
+
+ @Test
+ fun createReadPermissionInternal() {
+ val permission = HealthPermission.createReadPermissionInternal(StepsRecord::class)
+ assertThat(permission).isEqualTo(HealthPermission.READ_STEPS)
+ }
+
+ @Test
+ fun createReadPermissionInternal_everyRecord() {
+ RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
+ .forEach {
+ val permission = HealthPermission.createReadPermissionInternal(it)
+ assertThat(permission).isNotNull()
+ }
+ }
+
+ @Test
+ fun createReadPermissionInternal_invalidRecord_isNull() {
+ assertThrows(IllegalArgumentException::class.java) {
+ HealthPermission.createReadPermissionInternal(Record::class)
+ }
+ }
+
+ @Test
+ fun createWritePermissionInternal() {
+ val permission = HealthPermission.createWritePermissionInternal(StepsRecord::class)
+ assertThat(permission).isEqualTo(HealthPermission.WRITE_STEPS)
+ }
+
+ @Test
+ fun createWritePermissionInternal_everyRecord() {
+ RECORD_CLASSES.filterNot { it == ExerciseRouteRecord::class }
+ .forEach {
+ val permission = HealthPermission.createWritePermissionInternal(it)
+ assertThat(permission).isNotNull()
+ }
+ }
+
+ @Test
+ fun createWritePermissionInternal_invalidRecord_isNull() {
+ assertThrows(IllegalArgumentException::class.java) {
+ HealthPermission.createWritePermissionInternal(Record::class)
+ }
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
index d282950..9ebd4a4 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SleepStageRecordTest.kt
@@ -68,7 +68,7 @@
SleepStageRecord.Companion::class
.members
.asSequence()
- .filter { it -> it.name.startsWith("STAGE_TYPE") && !it.name.endsWith("UNKNOWN") }
+ .filter { it -> it.name.startsWith("STAGE_TYPE") }
.filter { it -> it.returnType == typeOf<Int>() }
.map { it -> it.call(ExerciseSessionRecord.Companion) }
.toHashSet()
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index a81ad20..609c83a5 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -28,6 +28,7 @@
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.Looper.getMainLooper
+import androidx.health.connect.client.permission.HealthPermission
import androidx.health.platform.client.impl.ipc.ClientConfiguration
import androidx.health.platform.client.impl.ipc.internal.ConnectionManager
import androidx.health.platform.client.impl.testing.FakeHealthDataService
@@ -123,6 +124,25 @@
}
@Test
+ fun filterGrantedPermissions_somePermissionsGranted_expectCorrectGrantedList() {
+ val readPermission =
+ PermissionProto.Permission.newBuilder()
+ .setPermission(HealthPermission.READ_HEART_RATE)
+ .build()
+ val writePermission =
+ PermissionProto.Permission.newBuilder()
+ .setPermission(HealthPermission.WRITE_BLOOD_PRESSURE)
+ .build()
+
+ fakeAhpServiceStub.addGrantedPermission(Permission(readPermission))
+ val resultFuture = ahpClient.getGrantedPermissions(setOf(readPermission, writePermission))
+ shadowOf(getMainLooper()).idle()
+
+ val expected = setOf(readPermission)
+ assertSuccess(resultFuture, expected)
+ }
+
+ @Test
fun revokeAllPermissions_success() {
val resultFuture: ListenableFuture<Unit> = ahpClient.revokeAllPermissions()
shadowOf(getMainLooper()).idle()
diff --git a/inspection/inspection-gradle-plugin/build.gradle b/inspection/inspection-gradle-plugin/build.gradle
index dc1c3a7..fe20a87 100644
--- a/inspection/inspection-gradle-plugin/build.gradle
+++ b/inspection/inspection-gradle-plugin/build.gradle
@@ -38,7 +38,6 @@
testImplementation(project(":internal-testutils-gradle-plugin"))
testImplementation(gradleTestKit())
- testImplementation(libs.testRunner)
testImplementation(libs.junit)
testImplementation(libs.kotlinTest)
}
diff --git a/leanback/leanback-paging/build.gradle b/leanback/leanback-paging/build.gradle
index 878ec32..0d04026 100644
--- a/leanback/leanback-paging/build.gradle
+++ b/leanback/leanback-paging/build.gradle
@@ -10,7 +10,7 @@
dependencies {
api("androidx.annotation:annotation:1.1.0")
api("androidx.leanback:leanback:1.1.0-beta01")
- api("androidx.paging:paging-runtime:3.0.0")
+ api("androidx.paging:paging-runtime:3.1.0")
// To avoid manifest merger warnings due to duplicate package names
// It can be removed if leanback library is updated to a newer version
diff --git a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
index d4ae23a..9b2d05e 100644
--- a/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
+++ b/leanback/leanback-paging/src/androidTest/java/androidx/leanback/paging/PagingDataAdapterTest.kt
@@ -122,7 +122,6 @@
// empty previous list.
assertEvents(
listOf(
- localLoadStatesOf(),
localLoadStatesOf(refreshLocal = LoadState.Loading),
localLoadStatesOf(
refreshLocal = LoadState.NotLoading(endOfPaginationReached = false)
diff --git a/libraryversions.toml b/libraryversions.toml
index 2d67f06..cc12935 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -19,7 +19,7 @@
CAR_APP = "1.3.0-beta02"
COLLECTION = "1.3.0-dev01"
COMPOSE = "1.4.0-alpha02"
-COMPOSE_COMPILER = "1.3.3"
+COMPOSE_COMPILER = "1.4.0-alpha01"
COMPOSE_MATERIAL3 = "1.1.0-alpha02"
COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
CONSTRAINTLAYOUT = "2.2.0-alpha05"
@@ -91,6 +91,7 @@
PRINT = "1.1.0-beta01"
PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
PRIVACYSANDBOX_TOOLS = "1.0.0-alpha01"
+PRIVACYSANDBOX_UI = "1.0.0-alpha01"
PROFILEINSTALLER = "1.3.0-alpha02"
RECOMMENDATION = "1.1.0-alpha01"
RECYCLERVIEW = "1.4.0-alpha01"
@@ -99,7 +100,7 @@
RESOURCEINSPECTION = "1.1.0-alpha01"
ROOM = "2.6.0-alpha01"
SAVEDSTATE = "1.3.0-alpha01"
-SECURITY = "1.1.0-alpha04"
+SECURITY = "1.1.0-alpha05"
SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
SECURITY_BIOMETRIC = "1.0.0-alpha01"
@@ -219,6 +220,7 @@
PRINT = { group = "androidx.print", atomicGroupVersion = "versions.PRINT" }
PRIVACYSANDBOX_SDKRUNTIME = { group = "androidx.privacysandbox.sdkruntime", atomicGroupVersion = "versions.PRIVACYSANDBOX_SDKRUNTIME" }
PRIVACYSANDBOX_TOOLS = { group = "androidx.privacysandbox.tools", atomicGroupVersion = "versions.PRIVACYSANDBOX_TOOLS" }
+PRIVACYSANDBOX_UI = { group = "androidx.privacysandbox.ui", atomicGroupVersion = "versions.PRIVACYSANDBOX_UI" }
PROFILEINSTALLER = { group = "androidx.profileinstaller", atomicGroupVersion = "versions.PROFILEINSTALLER" }
RECOMMENDATION = { group = "androidx.recommendation", atomicGroupVersion = "versions.RECOMMENDATION" }
RECYCLERVIEW = { group = "androidx.recyclerview" }
diff --git a/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt b/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt
new file mode 100644
index 0000000..561a1d3
--- /dev/null
+++ b/navigation/navigation-benchmark/src/androidTest/java/androidx/navigation/NavDeepLinkBenchmark.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.navigation
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NavDeepLinkBenchmark {
+
+ @get:Rule
+ val benchmarkRule = BenchmarkRule()
+
+ @Test
+ fun navGraphDestinations_withRoutes() = inflateNavGraph_withRoutes(1)
+
+ @Test
+ fun navGraphDestinations_withRoutes10() = inflateNavGraph_withRoutes(10)
+
+ @Test
+ fun navGraphDestinations_withRoutes50() = inflateNavGraph_withRoutes(50)
+
+ @Test
+ fun navGraphDestinations_withRoutes100() = inflateNavGraph_withRoutes(100)
+
+ private fun inflateNavGraph_withRoutes(count: Int) {
+ val navigatorProvider = NavigatorProvider().apply {
+ addNavigator(NavGraphNavigator(this))
+ addNavigator(NoOpNavigator())
+ }
+ val navigator = navigatorProvider.getNavigator(NoOpNavigator::class.java)
+ benchmarkRule.measureRepeated {
+ navigatorProvider.getNavigator(NavGraphNavigator::class.java)
+ .createDestination().apply {
+ id = GRAPH_ID
+ setStartDestination(START_DESTINATION_ID)
+ for (i in 0 until count) {
+ addDestination(
+ navigator.createDestination().apply {
+ route = URI_PATH + i + URI_EXTRAS
+ id = i
+ }
+ )
+ }
+ }
+ }
+ }
+
+ companion object {
+ const val URI_PATH = "example.com/"
+ const val URI_EXTRAS = "test/{test}?param1={param}#fragment"
+ const val START_DESTINATION_ID = 0
+ const val GRAPH_ID = 111
+ }
+}
\ No newline at end of file
diff --git a/navigation/navigation-dynamic-features-runtime/build.gradle b/navigation/navigation-dynamic-features-runtime/build.gradle
index 4adba73..6711b01 100644
--- a/navigation/navigation-dynamic-features-runtime/build.gradle
+++ b/navigation/navigation-dynamic-features-runtime/build.gradle
@@ -32,7 +32,7 @@
dependencies {
api(project(":navigation:navigation-runtime"))
- api(libs.playCore)
+ api(libs.playFeatureDelivery)
testImplementation(project(":navigation:navigation-testing"))
testImplementation("androidx.arch.core:core-testing:2.1.0")
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 0b3528e..856f8fd 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -26,19 +26,26 @@
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.TypeSpec
class AbstractSdkProviderGenerator(private val api: ParsedApi) {
companion object {
- private val SANDBOXED_SDK_PROVIDER_CLASS =
- ClassName("android.app.sdksandbox", "SandboxedSdkProvider")
- private val DATA_RECEIVED_CALLBACK_CLASS =
- ClassName("android.app.sdksandbox", "SandboxedSdkProvider", "DataReceivedCallback")
- private val SANDBOXED_SDK_CLASS =
- ClassName("android.app.sdksandbox", "SandboxedSdk")
- private val CONTEXT_CLASS = ClassName("android.content", "Context")
- private val BUNDLE_CLASS = ClassName("android.os", "Bundle")
- private val VIEW_CLASS = ClassName("android.view", "View")
+ private val sandboxedSdkProviderClass =
+ ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkProviderCompat")
+ private val sandboxedSdkClass =
+ ClassName("androidx.privacysandbox.sdkruntime.core", "SandboxedSdkCompat")
+ private val sandboxedSdkCreateMethod =
+ MemberName(
+ ClassName(
+ sandboxedSdkClass.packageName,
+ sandboxedSdkClass.simpleName,
+ "Companion"
+ ), "create"
+ )
+ private val contextClass = ClassName("android.content", "Context")
+ private val bundleClass = ClassName("android.os", "Bundle")
+ private val viewClass = ClassName("android.view", "View")
}
fun generate(): FileSpec? {
@@ -49,11 +56,10 @@
val className = "AbstractSandboxedSdkProvider"
val classSpec =
TypeSpec.classBuilder(className)
- .superclass(SANDBOXED_SDK_PROVIDER_CLASS)
+ .superclass(sandboxedSdkProviderClass)
.addModifiers(KModifier.ABSTRACT)
.addFunction(generateOnLoadSdkFunction())
.addFunction(generateGetViewFunction())
- .addFunction(generateOnDataReceivedFunction())
.addFunction(generateCreateServiceFunction(api.getOnlyService()))
return FileSpec.builder(packageName, className)
@@ -64,14 +70,14 @@
private fun generateOnLoadSdkFunction(): FunSpec {
return FunSpec.builder("onLoadSdk").build {
addModifiers(KModifier.OVERRIDE)
- addParameter("params", BUNDLE_CLASS)
- returns(SANDBOXED_SDK_CLASS)
+ addParameter("params", bundleClass)
+ returns(sandboxedSdkClass)
addStatement(
"val sdk = ${getCreateServiceFunctionName(api.getOnlyService())}(context!!)"
)
addStatement(
- "return %T(%T(sdk))",
- SANDBOXED_SDK_CLASS,
+ "return %M(%T(sdk))",
+ sandboxedSdkCreateMethod,
api.getOnlyService().stubDelegateNameSpec()
)
}
@@ -80,11 +86,11 @@
private fun generateGetViewFunction(): FunSpec {
return FunSpec.builder("getView").build {
addModifiers(KModifier.OVERRIDE)
- addParameter("windowContext", CONTEXT_CLASS)
- addParameter("params", BUNDLE_CLASS)
+ addParameter("windowContext", contextClass)
+ addParameter("params", bundleClass)
addParameter("width", Int::class)
addParameter("height", Int::class)
- returns(VIEW_CLASS)
+ returns(viewClass)
addStatement("TODO(\"Implement\")")
}
}
@@ -92,19 +98,11 @@
private fun generateCreateServiceFunction(service: AnnotatedInterface): FunSpec {
return FunSpec.builder(getCreateServiceFunctionName(service))
.addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED)
- .addParameter("context", CONTEXT_CLASS)
+ .addParameter("context", contextClass)
.returns(service.type.poetSpec())
.build()
}
- private fun generateOnDataReceivedFunction(): FunSpec {
- return FunSpec.builder("onDataReceived")
- .addModifiers(KModifier.OVERRIDE)
- .addParameter("data", BUNDLE_CLASS)
- .addParameter("callback", DATA_RECEIVED_CALLBACK_CLASS)
- .build()
- }
-
private fun getCreateServiceFunctionName(service: AnnotatedInterface) =
"create${service.type.simpleName}"
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
index c56f35ed..0d1b157 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/SdkCodeGenerator.kt
@@ -16,27 +16,32 @@
package androidx.privacysandbox.tools.apicompiler.generator
-import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.Metadata
import androidx.privacysandbox.tools.core.generator.AidlCompiler
import androidx.privacysandbox.tools.core.generator.AidlGenerator
-import androidx.privacysandbox.tools.core.generator.BinderCodeConverter
import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
+import androidx.privacysandbox.tools.core.generator.ServerBinderCodeConverter
import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
+import androidx.privacysandbox.tools.core.generator.ThrowableParcelConverterFileGenerator
import androidx.privacysandbox.tools.core.generator.TransportCancellationGenerator
import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
+import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.squareup.kotlinpoet.FileSpec
import java.nio.file.Files.createTempDirectory
import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.nameWithoutExtension
class SdkCodeGenerator(
private val codeGenerator: CodeGenerator,
private val api: ParsedApi,
private val aidlCompilerPath: Path,
) {
- private val binderCodeConverter = BinderCodeConverter(api)
+ private val binderCodeConverter = ServerBinderCodeConverter(api)
fun generate() {
if (api.services.isEmpty()) {
@@ -47,6 +52,8 @@
generateStubDelegates()
generateValueConverters()
generateCallbackProxies()
+ generateToolMetadata()
+ generateSuspendFunctionUtilities()
}
private fun generateAidlSources() {
@@ -74,28 +81,42 @@
}
private fun generateStubDelegates() {
- val basePackageName = api.getOnlyService().type.packageName
- val stubDelegateGenerator = StubDelegatesGenerator(basePackageName, binderCodeConverter)
+ val stubDelegateGenerator = StubDelegatesGenerator(basePackageName(), binderCodeConverter)
api.services.map(stubDelegateGenerator::generate).forEach(::write)
api.interfaces.map(stubDelegateGenerator::generate).forEach(::write)
-
- val transportCancellationGenerator = TransportCancellationGenerator(basePackageName)
- transportCancellationGenerator.generate().also(::write)
}
private fun generateValueConverters() {
- val valueConverterFileGenerator = ValueConverterFileGenerator(api)
+ val valueConverterFileGenerator = ValueConverterFileGenerator(binderCodeConverter)
api.values.map(valueConverterFileGenerator::generate).forEach(::write)
}
private fun generateCallbackProxies() {
- val basePackageName = api.getOnlyService().type.packageName
- val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName, binderCodeConverter)
+ val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName(), binderCodeConverter)
api.callbacks.map(clientProxyGenerator::generate).forEach(::write)
}
- private fun write(spec: FileSpec) {
- codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
- .write(spec)
+ private fun generateToolMetadata() {
+ codeGenerator.createNewFile(
+ Dependencies(false),
+ Metadata.filePath.parent.toString(),
+ Metadata.filePath.nameWithoutExtension,
+ Metadata.filePath.extension,
+ ).use { Metadata.toolMetadata.writeTo(it) }
}
+
+ private fun generateSuspendFunctionUtilities() {
+ if (!api.hasSuspendFunctions()) return
+ TransportCancellationGenerator(basePackageName()).generate().also(::write)
+ ThrowableParcelConverterFileGenerator(basePackageName()).generate(convertToParcel = true)
+ .also(::write)
+ }
+
+ private fun write(spec: FileSpec) {
+
+ codeGenerator.createNewFile(Dependencies(false), spec.packageName, spec.name)
+ .bufferedWriter().use(spec::writeTo)
+ }
+
+ private fun basePackageName() = api.getOnlyService().type.packageName
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 4fd38fd..36dbbfb 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -16,12 +16,15 @@
package androidx.privacysandbox.tools.apicompiler
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import androidx.privacysandbox.tools.testing.resourceOutputDir
+import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
import androidx.room.compiler.processing.util.compiler.compile
-import androidx.room.compiler.processing.util.Source
+import com.google.common.truth.Truth.assertThat
import java.io.File
import java.nio.file.Files
import org.junit.Test
@@ -66,6 +69,9 @@
"com/mysdk/IResponseTransactionCallback.java",
"com/mysdk/MyCallbackClientProxy.kt",
"com/mysdk/IMyCallback.java",
+ "com/mysdk/PrivacySandboxThrowableParcelConverter.kt",
+ "com/mysdk/ParcelableStackFrame.java",
+ "com/mysdk/PrivacySandboxThrowableParcel.java",
)
}.also {
it.generatesSourcesWithContents(expectedOutput)
@@ -89,6 +95,43 @@
}
@Test
+ fun generatesMetadataFile() {
+ val source =
+ Source.kotlin(
+ "com/mysdk/MySdk.kt",
+ """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+ @PrivacySandboxService
+ interface MySdk {
+ fun doStuff(x: Int, y: Int)
+ }
+ """
+ )
+ val provider = PrivacySandboxKspCompiler.Provider()
+ val compilationResult =
+ compileAll(
+ listOf(source),
+ symbolProcessorProviders = listOf(provider),
+ processorOptions = getProcessorOptions(),
+ )
+ assertThat(compilationResult).succeeds()
+
+ val resourceMap = compilationResult.resourceOutputDir.walk()
+ .filter { it.isFile }
+ .map { it.toRelativeString(compilationResult.resourceOutputDir) to it.readBytes() }
+ .toMap()
+ val expectedMetadataRelativePath = "META-INF/privacysandbox/tool-metadata.pb"
+ assertThat(resourceMap).containsKey(expectedMetadataRelativePath)
+ assertThat(ToolMetadata.parseFrom(resourceMap[expectedMetadataRelativePath]))
+ .isEqualTo(
+ ToolMetadata.newBuilder()
+ .setCodeGenerationVersion(1)
+ .build()
+ )
+ }
+
+ @Test
fun compileInvalidServiceInterface_fails() {
val source =
Source.kotlin(
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
index abdec6c..5dd00bf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/parser/ValueParserTest.kt
@@ -178,8 +178,9 @@
)
checkSourceFails(dataClass)
.containsExactlyErrors(
- "Error in com.mysdk.MySdkRequest.foo: only primitives and data classes " +
- "annotated with @PrivacySandboxValue are supported as properties."
+ "Error in com.mysdk.MySdkRequest.foo: only primitives, data classes " +
+ "annotated with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as properties."
)
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
index 94386a63..94ec579 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -39,14 +39,16 @@
}
@PrivacySandboxValue
-data class Request(val query: String)
+data class Request(val query: String, val myInterface: MyInterface)
@PrivacySandboxValue
-data class Response(val response: String)
+data class Response(val response: String, val mySecondInterface: MySecondInterface)
@PrivacySandboxCallback
interface MyCallback {
fun onComplete(response: Response)
fun onClick(x: Int, y: Int)
+
+ fun onCompleteInterface(myInterface: MyInterface)
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
index 97ab29e..6efe8a0 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/AbstractSandboxedSdkProvider.kt
@@ -1,17 +1,17 @@
package com.mysdk
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SandboxedSdkProvider
import android.content.Context
import android.os.Bundle
import android.view.View
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat.Companion.create
+import androidx.privacysandbox.sdkruntime.core.SandboxedSdkProviderCompat
import kotlin.Int
-import kotlin.Unit
-public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
- public override fun onLoadSdk(params: Bundle): SandboxedSdk {
+public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProviderCompat() {
+ public override fun onLoadSdk(params: Bundle): SandboxedSdkCompat {
val sdk = createMySdk(context!!)
- return SandboxedSdk(MySdkStubDelegate(sdk))
+ return create(MySdkStubDelegate(sdk))
}
public override fun getView(
@@ -23,9 +23,5 @@
TODO("Implement")
}
- public override fun onDataReceived(`data`: Bundle,
- callback: SandboxedSdkProvider.DataReceivedCallback): Unit {
- }
-
protected abstract fun createMySdk(context: Context): MySdk
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
index 6fa273f..2f861da 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyCallbackClientProxy.kt
@@ -1,12 +1,9 @@
package com.mysdk
import com.mysdk.ResponseConverter.toParcelable
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
public class MyCallbackClientProxy(
- private val remote: IMyCallback,
+ public val remote: IMyCallback,
) : MyCallback {
public override fun onComplete(response: Response): Unit {
remote.onComplete(toParcelable(response))
@@ -15,4 +12,8 @@
public override fun onClick(x: Int, y: Int): Unit {
remote.onClick(x, y)
}
+
+ public override fun onCompleteInterface(myInterface: MyInterface): Unit {
+ remote.onCompleteInterface(MyInterfaceStubDelegate(myInterface))
+ }
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
index 7b71956..2c9cf05 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MyInterfaceStubDelegate.kt
@@ -1,9 +1,12 @@
package com.mysdk
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
import com.mysdk.RequestConverter.fromParcelable
import com.mysdk.ResponseConverter.toParcelable
import kotlin.Int
import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -13,13 +16,14 @@
) : IMyInterface.Stub() {
public override fun doSomething(request: ParcelableRequest,
transactionCallback: IResponseTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.doSomething(fromParcelable(request))
transactionCallback.onSuccess(toParcelable(result))
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -28,13 +32,14 @@
public override fun getMyInterface(input: IMyInterface,
transactionCallback: IMyInterfaceTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.getMyInterface((input as MyInterfaceStubDelegate).delegate)
transactionCallback.onSuccess(MyInterfaceStubDelegate(result))
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -43,6 +48,7 @@
public override fun getMySecondInterface(input: IMySecondInterface,
transactionCallback: IMySecondInterfaceTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.getMySecondInterface((input as
@@ -50,7 +56,7 @@
transactionCallback.onSuccess(MySecondInterfaceStubDelegate(result))
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
index 5228c52..0f4c3ee 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -1,10 +1,13 @@
package com.mysdk
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.toThrowableParcel
import com.mysdk.RequestConverter
import com.mysdk.RequestConverter.fromParcelable
import com.mysdk.ResponseConverter.toParcelable
import kotlin.Int
import kotlin.Unit
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -17,13 +20,14 @@
y: Int,
transactionCallback: IStringTransactionCallback,
): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.doStuff(x, y)
transactionCallback.onSuccess(result)
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -32,13 +36,14 @@
public override fun handleRequest(request: ParcelableRequest,
transactionCallback: IResponseTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.handleRequest(fromParcelable(request))
transactionCallback.onSuccess(toParcelable(result))
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -47,13 +52,14 @@
public override fun logRequest(request: ParcelableRequest,
transactionCallback: IUnitTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
- val result = delegate.logRequest(fromParcelable(request))
+ delegate.logRequest(fromParcelable(request))
transactionCallback.onSuccess()
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
@@ -70,13 +76,14 @@
public override fun getMyInterface(input: IMyInterface,
transactionCallback: IMyInterfaceTransactionCallback): Unit {
+ @OptIn(DelicateCoroutinesApi::class)
val job = GlobalScope.launch(Dispatchers.Main) {
try {
val result = delegate.getMyInterface((input as MyInterfaceStubDelegate).delegate)
transactionCallback.onSuccess(MyInterfaceStubDelegate(result))
}
catch (t: Throwable) {
- transactionCallback.onFailure(404, t.message)
+ transactionCallback.onFailure(toThrowableParcel(t))
}
}
val cancellationSignal = TransportCancellationCallback() { job.cancel() }
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..368f02f
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,25 @@
+package com.mysdk
+
+public object PrivacySandboxThrowableParcelConverter {
+ public fun toThrowableParcel(throwable: Throwable): PrivacySandboxThrowableParcel {
+ val parcel = PrivacySandboxThrowableParcel()
+ parcel.exceptionClass = throwable::class.qualifiedName
+ parcel.errorMessage = throwable.message
+ parcel.stackTrace = throwable.stackTrace.map {
+ val stackFrame = ParcelableStackFrame()
+ stackFrame.declaringClass = it.className
+ stackFrame.methodName = it.methodName
+ stackFrame.fileName = it.fileName
+ stackFrame.lineNumber = it.lineNumber
+ stackFrame
+ }.toTypedArray()
+ throwable.cause?.let {
+ parcel.cause = toThrowableParcel(it)
+ }
+ parcel.suppressedExceptions =
+ throwable.suppressedExceptions.map {
+ toThrowableParcel(it)
+ }.toTypedArray()
+ return parcel
+ }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt
new file mode 100644
index 0000000..538f1cd
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/RequestConverter.kt
@@ -0,0 +1,17 @@
+package com.mysdk
+
+public object RequestConverter {
+ public fun fromParcelable(parcelable: ParcelableRequest): Request {
+ val annotatedValue = Request(
+ query = parcelable.query,
+ myInterface = (parcelable.myInterface as MyInterfaceStubDelegate).delegate)
+ return annotatedValue
+ }
+
+ public fun toParcelable(annotatedValue: Request): ParcelableRequest {
+ val parcelable = ParcelableRequest()
+ parcelable.query = annotatedValue.query
+ parcelable.myInterface = MyInterfaceStubDelegate(annotatedValue.myInterface)
+ return parcelable
+ }
+}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt
new file mode 100644
index 0000000..3138ba1
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/ResponseConverter.kt
@@ -0,0 +1,19 @@
+package com.mysdk
+
+public object ResponseConverter {
+ public fun fromParcelable(parcelable: ParcelableResponse): Response {
+ val annotatedValue = Response(
+ response = parcelable.response,
+ mySecondInterface = (parcelable.mySecondInterface as
+ MySecondInterfaceStubDelegate).delegate)
+ return annotatedValue
+ }
+
+ public fun toParcelable(annotatedValue: Response): ParcelableResponse {
+ val parcelable = ParcelableResponse()
+ parcelable.response = annotatedValue.response
+ parcelable.mySecondInterface =
+ MySecondInterfaceStubDelegate(annotatedValue.mySecondInterface)
+ return parcelable
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
index 6417ee2..56e1a34 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/PrivacySandboxApiGenerator.kt
@@ -17,20 +17,29 @@
package androidx.privacysandbox.tools.apigenerator
import androidx.privacysandbox.tools.apigenerator.parser.ApiStubParser
-import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.generator.AidlCompiler
import androidx.privacysandbox.tools.core.generator.AidlGenerator
import androidx.privacysandbox.tools.core.generator.BinderCodeConverter
+import androidx.privacysandbox.tools.core.generator.ClientBinderCodeConverter
import androidx.privacysandbox.tools.core.generator.ClientProxyTypeGenerator
+import androidx.privacysandbox.tools.core.generator.PrivacySandboxExceptionFileGenerator
import androidx.privacysandbox.tools.core.generator.StubDelegatesGenerator
+import androidx.privacysandbox.tools.core.generator.ThrowableParcelConverterFileGenerator
import androidx.privacysandbox.tools.core.generator.ValueConverterFileGenerator
import androidx.privacysandbox.tools.core.generator.ValueFileGenerator
+import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
import java.io.File
-import java.nio.file.Files
import java.nio.file.Path
+import java.util.zip.ZipInputStream
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
import kotlin.io.path.exists
+import kotlin.io.path.inputStream
import kotlin.io.path.isDirectory
+import kotlin.io.path.moveTo
+import kotlin.io.path.outputStream
/** Generate source files for communicating with an SDK running in the Privacy Sandbox. */
class PrivacySandboxApiGenerator {
@@ -54,14 +63,15 @@
"$outputDirectory is not a valid output path."
}
+ val api = unzipDescriptorsFileAndParseStubs(sdkInterfaceDescriptors, outputDirectory)
val output = outputDirectory.toFile()
- val api = ApiStubParser.parse(sdkInterfaceDescriptors)
val basePackageName = api.getOnlyService().type.packageName
- val binderCodeConverter = BinderCodeConverter(api)
+ val binderCodeConverter = ClientBinderCodeConverter(api)
val interfaceFileGenerator = InterfaceFileGenerator()
generateBinders(api, AidlCompiler(aidlCompiler), output)
+ generateServiceFactory(api, output)
generateStubDelegates(
api,
basePackageName,
@@ -76,7 +86,8 @@
interfaceFileGenerator,
output
)
- generateValueConverters(api, output)
+ generateValueConverters(api, binderCodeConverter, output)
+ generateSuspendFunctionUtilities(api, basePackageName, output)
}
private fun generateBinders(api: ParsedApi, aidlCompiler: AidlCompiler, output: File) {
@@ -88,14 +99,21 @@
val relativePath = aidlWorkingDir.toPath().relativize(it.file.toPath())
val source = it.file.toPath()
val dest = output.toPath().resolve(relativePath)
- dest.toFile().parentFile.mkdirs()
- Files.move(source, dest)
+ dest.parent.createDirectories()
+ source.moveTo(dest)
}
} finally {
aidlWorkingDir.deleteRecursively()
}
}
+ private fun generateServiceFactory(api: ParsedApi, output: File) {
+ val serviceFactoryFileGenerator = ServiceFactoryFileGenerator()
+ api.services.forEach {
+ serviceFactoryFileGenerator.generate(it).writeTo(output)
+ }
+ }
+
private fun generateStubDelegates(
api: ParsedApi,
basePackageName: String,
@@ -118,20 +136,58 @@
output: File
) {
val clientProxyGenerator = ClientProxyTypeGenerator(basePackageName, binderCodeConverter)
- val serviceFactoryFileGenerator = ServiceFactoryFileGenerator()
- api.services.forEach {
+ val annotatedInterfaces = api.services + api.interfaces
+ annotatedInterfaces.forEach {
interfaceFileGenerator.generate(it).writeTo(output)
clientProxyGenerator.generate(it).writeTo(output)
- serviceFactoryFileGenerator.generate(it).writeTo(output)
}
}
- private fun generateValueConverters(api: ParsedApi, output: File) {
+ private fun generateValueConverters(
+ api: ParsedApi,
+ binderCodeConverter: BinderCodeConverter,
+ output: File
+ ) {
val valueFileGenerator = ValueFileGenerator()
- val valueConverterFileGenerator = ValueConverterFileGenerator(api)
+ val valueConverterFileGenerator = ValueConverterFileGenerator(binderCodeConverter)
api.values.forEach {
valueFileGenerator.generate(it).writeTo(output)
valueConverterFileGenerator.generate(it).writeTo(output)
}
}
+
+ private fun unzipDescriptorsFileAndParseStubs(
+ sdkInterfaceDescriptors: Path,
+ outputDirectory: Path,
+ ): ParsedApi {
+ val workingDirectory = outputDirectory.resolve("tmp-descriptors").createDirectories()
+ try {
+ ZipInputStream(sdkInterfaceDescriptors.inputStream()).use { input ->
+ generateSequence { input.nextEntry }
+ .forEach { zipEntry ->
+ val destination = workingDirectory.resolve(zipEntry.name)
+ check(destination.startsWith(workingDirectory))
+ destination.parent?.createDirectories()
+ destination.createFile()
+ input.copyTo(destination.outputStream())
+ }
+ }
+
+ return ApiStubParser.parse(workingDirectory)
+ } finally {
+ workingDirectory.toFile().deleteRecursively()
+ }
+ }
+
+ private fun generateSuspendFunctionUtilities(
+ api: ParsedApi,
+ basePackageName: String,
+ output: File
+ ) {
+ if (!api.hasSuspendFunctions()) return
+ ThrowableParcelConverterFileGenerator(basePackageName).generate(
+ convertFromParcel = true
+ ).writeTo(output)
+ PrivacySandboxExceptionFileGenerator(basePackageName).generate().writeTo(output)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
index 89baf13..293d0ad 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/ServiceFactoryFileGenerator.kt
@@ -16,86 +16,32 @@
package androidx.privacysandbox.tools.apigenerator
-import androidx.privacysandbox.tools.core.generator.addCode
+import androidx.privacysandbox.tools.core.generator.SpecNames.iBinderClassName
import androidx.privacysandbox.tools.core.generator.addCommonSettings
-import androidx.privacysandbox.tools.core.generator.addControlFlow
-import androidx.privacysandbox.tools.core.generator.addStatement
import androidx.privacysandbox.tools.core.generator.aidlInterfaceNameSpec
import androidx.privacysandbox.tools.core.generator.build
import androidx.privacysandbox.tools.core.generator.clientProxyNameSpec
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import com.squareup.kotlinpoet.ClassName
-import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterSpec
-import com.squareup.kotlinpoet.joinToCode
-internal class ServiceFactoryFileGenerator() {
+internal class ServiceFactoryFileGenerator {
+
fun generate(service: AnnotatedInterface): FileSpec =
FileSpec.builder(service.type.packageName, "${service.type.simpleName}Factory").build {
addCommonSettings()
- addImport("kotlinx.coroutines", "suspendCancellableCoroutine")
- addImport("kotlin.coroutines", "resume")
- addImport("kotlin.coroutines", "resumeWithException")
-
addFunction(generateFactoryFunction(service))
}
private fun generateFactoryFunction(service: AnnotatedInterface) =
- FunSpec.builder("create${service.type.simpleName}").build {
- addModifiers(KModifier.SUSPEND)
- addParameter(ParameterSpec("context", AndroidClassNames.context))
+ FunSpec.builder("wrapTo${service.type.simpleName}").build {
+ addParameter(ParameterSpec("binder", iBinderClassName))
returns(ClassName(service.type.packageName, service.type.simpleName))
-
- addCode {
- addControlFlow("return suspendCancellableCoroutine") {
- addStatement(
- "val sdkSandboxManager = context.getSystemService(%T::class.java)",
- AndroidClassNames.sandboxManager
- )
- addControlFlow(
- "val outcomeReceiver = object: %T<%T, %T>",
- AndroidClassNames.outcomeReceiver,
- AndroidClassNames.sandboxedSdk,
- AndroidClassNames.loadSdkException
- ) {
- addControlFlow(
- "override fun onResult(result: %T)", AndroidClassNames.sandboxedSdk
- ) {
- addStatement(
- "it.resume(%T(%T.Stub.asInterface(result.getInterface())))",
- service.clientProxyNameSpec(), service.aidlInterfaceNameSpec()
- )
- }
- addControlFlow(
- "override fun onError(error: %T)", AndroidClassNames.loadSdkException
- ) {
- addStatement("it.resumeWithException(error)")
- }
- }
- val loadSdkParameters = listOf(
- CodeBlock.of("%S", service.type.packageName),
- CodeBlock.of("%T.EMPTY", AndroidClassNames.bundle),
- CodeBlock.of("Runnable::run"),
- CodeBlock.of("outcomeReceiver"),
- )
- addStatement {
- add("sdkSandboxManager.loadSdk(")
- add(loadSdkParameters.joinToCode())
- add(")")
- }
- }
- }
+ addStatement(
+ "return %T(%T.Stub.asInterface(binder))",
+ service.clientProxyNameSpec(), service.aidlInterfaceNameSpec()
+ )
}
}
-
-private object AndroidClassNames {
- val context = ClassName("android.content", "Context")
- val bundle = ClassName("android.os", "Bundle")
- val outcomeReceiver = ClassName("android.os", "OutcomeReceiver")
- val sandboxManager = ClassName("android.app.sdksandbox", "SdkSandboxManager")
- val sandboxedSdk = ClassName("android.app.sdksandbox", "SandboxedSdk")
- val loadSdkException = ClassName("android.app.sdksandbox", "LoadSdkException")
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
index 38b8a19..b756b24 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/AnnotatedClassReader.kt
@@ -17,10 +17,10 @@
package androidx.privacysandbox.tools.apigenerator.parser
import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
import java.nio.file.Path
-import java.util.zip.ZipInputStream
import kotlinx.metadata.KmClass
import kotlinx.metadata.jvm.KotlinClassHeader
import kotlinx.metadata.jvm.KotlinClassMetadata
@@ -34,7 +34,9 @@
val services: Set<KmClass>,
val values: Set<KmClass>,
val callbacks: Set<KmClass>,
+ val interfaces: Set<KmClass>,
)
+
internal object AnnotatedClassReader {
val annotations = listOf(PrivacySandboxService::class)
@@ -42,7 +44,11 @@
val services = mutableSetOf<KmClass>()
val values = mutableSetOf<KmClass>()
val callbacks = mutableSetOf<KmClass>()
- readClassNodes(stubClassPath).forEach { classNode ->
+ val interfaces = mutableSetOf<KmClass>()
+ stubClassPath.toFile().walk()
+ .filter { it.extension == "class" }
+ .map { toClassNode(it.readBytes()) }
+ .forEach { classNode ->
if (classNode.isAnnotatedWith<PrivacySandboxService>()) {
services.add(parseKotlinMetadata(classNode))
}
@@ -52,19 +58,18 @@
if (classNode.isAnnotatedWith<PrivacySandboxCallback>()) {
callbacks.add(parseKotlinMetadata(classNode))
}
+ if (classNode.isAnnotatedWith<PrivacySandboxInterface>()) {
+ interfaces.add(parseKotlinMetadata(classNode))
+ }
}
- return AnnotatedClasses(services.toSet(), values.toSet(), callbacks.toSet())
+ return AnnotatedClasses(
+ services = services.toSet(),
+ values = values.toSet(),
+ callbacks = callbacks.toSet(),
+ interfaces = interfaces.toSet()
+ )
}
- private fun readClassNodes(stubClassPath: Path): List<ClassNode> =
- ZipInputStream(stubClassPath.toFile().inputStream()).use { input ->
- generateSequence { input.nextEntry }
- .filter { it.name.endsWith(".class") }
- .map {
- toClassNode(input.readAllBytes())
- }.toList()
- }
-
private fun toClassNode(classContents: ByteArray): ClassNode {
val reader = ClassReader(classContents)
val classNode = ClassNode(Opcodes.ASM9)
diff --git a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
index 6a3fa930..1db70b6 100644
--- a/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/main/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParser.kt
@@ -37,13 +37,11 @@
/**
* Parses the API annotated by a Privacy Sandbox SDK from its compiled classes.
*
- * @param sdkInterfaceDescriptors Path to SDK interface descriptors. This should be a jar
- * file with a set of compiled SDK classes and at least one of them should be a valid
- * Kotlin interface annotated with @PrivacySandboxService.
+ * @param sdkStubsClasspath root directory of SDK classpath.
*/
- internal fun parse(sdkInterfaceDescriptors: Path): ParsedApi {
- val (services, values, callbacks) =
- AnnotatedClassReader.readAnnotatedClasses(sdkInterfaceDescriptors)
+ internal fun parse(sdkStubsClasspath: Path): ParsedApi {
+ val (services, values, callbacks, interfaces) =
+ AnnotatedClassReader.readAnnotatedClasses(sdkStubsClasspath)
if (services.isEmpty()) throw PrivacySandboxParsingException(
"Unable to find valid interfaces annotated with @PrivacySandboxService."
)
@@ -51,6 +49,7 @@
services.map { parseInterface(it, "PrivacySandboxService") }.toSet(),
values.map(::parseValue).toSet(),
callbacks.map { parseInterface(it, "PrivacySandboxCallback") }.toSet(),
+ interfaces.map { parseInterface(it, "PrivacySandboxInterface") }.toSet(),
).also(::validate)
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
index 005e8b9..a647718 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
@@ -16,10 +16,12 @@
package androidx.privacysandbox.tools.apigenerator
+import androidx.privacysandbox.tools.core.Metadata
import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
import androidx.room.compiler.processing.util.Source
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import java.io.File
import java.nio.file.Files
import kotlin.io.path.Path
@@ -33,7 +35,8 @@
private val generatedSources: List<Source> by lazy {
val descriptors =
compileIntoInterfaceDescriptorsJar(
- *loadSourcesFromDirectory(inputDirectory).toTypedArray()
+ loadSourcesFromDirectory(inputDirectory),
+ mapOf(Metadata.filePath to Metadata.toolMetadata.toByteArray())
)
val aidlPath = System.getProperty("aidl_compiler_path")?.let(::Path)
?: throw IllegalArgumentException("aidl_compiler_path flag not set.")
@@ -47,7 +50,7 @@
@Test
fun generatedApi_compiles() {
- assertCompiles(generatedSources)
+ assertCompiles(generatedSources)
}
@Test
@@ -60,7 +63,10 @@
val outputSourceMap = generatedSources.associateBy(Source::relativePath)
for (expected in expectedSources) {
- assertThat(outputSourceMap[expected.relativePath]?.contents)
+ assertWithMessage(
+ "Contents of generated file %s don't match goldens.",
+ expected.relativePath
+ ).that(outputSourceMap[expected.relativePath]?.contents)
.isEqualTo(expected.contents)
}
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
index bc17d9d..03bc25d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
@@ -25,10 +25,9 @@
override val inputDirectory = File("src/test/test-data/callbacks/input")
override val outputDirectory = File("src/test/test-data/callbacks/output")
override val relativePathsToExpectedAidlClasses = listOf(
- "com/sdkwithcallbacks/ParcelableResponse.java",
+ "com/sdkwithcallbacks/IMyInterface.java",
"com/sdkwithcallbacks/ISdkCallback.java",
- "com/sdkwithcallbacks/ICancellationSignal.java",
- "com/sdkwithcallbacks/IUnitTransactionCallback.java",
"com/sdkwithcallbacks/ISdkService.java",
+ "com/sdkwithcallbacks/ParcelableResponse.java",
)
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
new file mode 100644
index 0000000..dea1e4a
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.apigenerator
+
+import java.io.File
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class InterfaceApiGeneratorTest : BaseApiGeneratorTest() {
+ override val inputDirectory = File("src/test/test-data/interfaces/input")
+ override val outputDirectory = File("src/test/test-data/interfaces/output")
+ override val relativePathsToExpectedAidlClasses = listOf(
+ "com/sdk/IMySdk.java",
+ "com/sdk/IMyInterface.java",
+ "com/sdk/IMySecondInterface.java",
+ "com/sdk/IMyInterfaceTransactionCallback.java",
+ "com/sdk/IIntTransactionCallback.java",
+ "com/sdk/ICancellationSignal.java",
+ "com/sdk/ParcelableStackFrame.java",
+ "com/sdk/PrivacySandboxThrowableParcel.java",
+ )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
index 411a357..955a26b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
@@ -29,5 +29,7 @@
"com/mysdk/IBooleanTransactionCallback.java",
"com/mysdk/ICancellationSignal.java",
"com/mysdk/IUnitTransactionCallback.java",
+ "com/mysdk/ParcelableStackFrame.java",
+ "com/mysdk/PrivacySandboxThrowableParcel.java",
)
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index 5c13a3f..fade128 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -19,15 +19,52 @@
import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager
import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
import java.nio.file.Files.createTempDirectory
import java.nio.file.Path
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
+import kotlin.io.path.writeBytes
-fun compileIntoInterfaceDescriptorsJar(vararg sources: Source): Path {
+/**
+ * Compiles the given [sources] and creates a packaged SDK API descriptors jar.
+ *
+ * @param descriptorResources map of extra resources that will be added to descriptors jar keyed by
+ * their relative path.
+ */
+fun compileIntoInterfaceDescriptorsJar(
+ sources: List<Source>,
+ descriptorResources: Map<Path, ByteArray> = mapOf(),
+): Path {
val tempDir = createTempDirectory("compile").also { it.toFile().deleteOnExit() }
val result = assertCompiles(sources.toList())
val sdkInterfaceDescriptors = tempDir.resolve("sdk-interface-descriptors.jar")
+ val outputClasspath = mergedClasspath(result)
+ descriptorResources.forEach { (relativePath, contents) ->
+ outputClasspath.resolve(relativePath).apply {
+ parent?.createDirectories()
+ createFile()
+ writeBytes(contents)
+ }
+ }
PrivacySandboxApiPackager().packageSdkDescriptors(
- result.outputClasspath.first().toPath(), sdkInterfaceDescriptors
+ outputClasspath, sdkInterfaceDescriptors
)
return sdkInterfaceDescriptors
}
+
+/**
+ * Merges all class paths from a compilation result into a single one.
+ *
+ * Room's compilation library returns different class paths for Kotlin and Java, so we need to
+ * merge them for tests that depend on the two. This is a naive implementation that simply
+ * overwrites classes that appear in multiple class paths.
+ */
+fun mergedClasspath(compilationResult: TestCompilationResult): Path {
+ val outputClasspath = createTempDirectory("classpath").also { it.toFile().deleteOnExit() }
+ compilationResult.outputClasspath
+ .forEach { classpath ->
+ classpath.copyRecursively(outputClasspath.toFile(), overwrite = true)
+ }
+ return outputClasspath
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
index c6a0b26..6eb5997 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
@@ -25,11 +25,14 @@
override val inputDirectory = File("src/test/test-data/values/input")
override val outputDirectory = File("src/test/test-data/values/output")
override val relativePathsToExpectedAidlClasses = listOf(
+ "com/sdkwithvalues/IMyInterface.java",
"com/sdkwithvalues/ISdkInterface.java",
"com/sdkwithvalues/ISdkResponseTransactionCallback.java",
"com/sdkwithvalues/ParcelableInnerSdkValue.java",
"com/sdkwithvalues/ParcelableSdkRequest.java",
"com/sdkwithvalues/ParcelableSdkResponse.java",
"com/sdkwithvalues/ICancellationSignal.java",
+ "com/sdkwithvalues/ParcelableStackFrame.java",
+ "com/sdkwithvalues/PrivacySandboxThrowableParcel.java",
)
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt
new file mode 100644
index 0000000..33cf59b
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/VersionCompatibilityCheckTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.apigenerator
+
+import androidx.privacysandbox.tools.core.Metadata
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
+import androidx.room.compiler.processing.util.Source
+import androidx.testutils.assertThrows
+import java.nio.file.Files
+import java.nio.file.Path
+import kotlin.io.path.Path
+import org.junit.Ignore
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class VersionCompatibilityCheckTest {
+ private val validSources = listOf(
+ Source.kotlin(
+ "com/mysdk/TestSandboxSdk.kt", """
+ package com.mysdk
+ import androidx.privacysandbox.tools.PrivacySandboxService
+ @PrivacySandboxService
+ interface MySdk {
+ fun doSomething(number: Int)
+ }
+ """
+ )
+ )
+
+ private val validMetadataContent = Metadata.toolMetadata.toByteArray()
+
+ @Test
+ @Ignore("b/255740194")
+ fun sdkDescriptorWithMissingMetadata_throws() {
+ assertThrows<IllegalArgumentException> {
+ runGeneratorWithResources(mapOf())
+ }.hasMessageThat().contains("Missing tool metadata in SDK API descriptor")
+ }
+
+ @Test
+ @Ignore("b/255740194")
+ fun sdkDescriptorWithMetadataInWrongPath_throws() {
+ assertThrows<IllegalArgumentException> {
+ runGeneratorWithResources(
+ mapOf(Path("invalid/dir/metadata.pb") to validMetadataContent)
+ )
+ }.hasMessageThat().contains("Missing tool metadata in SDK API descriptor")
+ }
+
+ @Test
+ @Ignore("b/255740194")
+ fun sdkDescriptorWithInvalidMetadataContent_throws() {
+ assertThrows<IllegalArgumentException> {
+ runGeneratorWithResources(
+ mapOf(Metadata.filePath to "bogus data".toByteArray())
+ )
+ }.hasMessageThat().contains("Invalid Privacy Sandbox tool metadata")
+ }
+
+ @Test
+ @Ignore("b/255740194")
+ fun sdkDescriptorWithIncompatibleVersion_throws() {
+ val sdkMetadata = ToolMetadata.newBuilder()
+ .setCodeGenerationVersion(999)
+ .build()
+ assertThrows<IllegalArgumentException> {
+ runGeneratorWithResources(
+ mapOf(Metadata.filePath to sdkMetadata.toByteArray())
+ )
+ }.hasMessageThat().contains(
+ "SDK uses incompatible Privacy Sandbox tooling (version 999)"
+ )
+ }
+
+ @Test
+ @Ignore("b/255740194")
+ fun sdkDescriptorWithLowerVersion_isCompatible() {
+ val sdkMetadata = ToolMetadata.newBuilder()
+ .setCodeGenerationVersion(0)
+ .build()
+ runGeneratorWithResources(mapOf(Metadata.filePath to sdkMetadata.toByteArray()))
+ }
+
+ private fun runGeneratorWithResources(resources: Map<Path, ByteArray>) {
+ val aidlPath = System.getProperty("aidl_compiler_path")?.let(::Path)
+ ?: throw IllegalArgumentException("aidl_compiler_path flag not set.")
+ val descriptors = compileIntoInterfaceDescriptorsJar(validSources, resources)
+ val generator = PrivacySandboxApiGenerator()
+ val outputDir = Files.createTempDirectory("output").also { it.toFile().deleteOnExit() }
+ generator.generate(descriptors, aidlPath, outputDir)
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
index 3489840..cc006e8 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/parser/ApiStubParserTest.kt
@@ -16,7 +16,7 @@
package androidx.privacysandbox.tools.apigenerator.parser
-import androidx.privacysandbox.tools.apigenerator.compileIntoInterfaceDescriptorsJar
+import androidx.privacysandbox.tools.apigenerator.mergedClasspath
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.AnnotatedValue
import androidx.privacysandbox.tools.core.model.Method
@@ -25,6 +25,7 @@
import androidx.privacysandbox.tools.core.model.Type
import androidx.privacysandbox.tools.core.model.Types
import androidx.privacysandbox.tools.core.model.ValueProperty
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import androidx.room.compiler.processing.util.Source
import androidx.testutils.assertThrows
import com.google.common.truth.Truth.assertThat
@@ -40,12 +41,18 @@
"com/mysdk/TestSandboxSdk.kt", """
package com.mysdk
import androidx.privacysandbox.tools.PrivacySandboxCallback
+ import androidx.privacysandbox.tools.PrivacySandboxInterface
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
@PrivacySandboxService
interface MySdk {
fun doSomething(magicNumber: Int, awesomeString: String)
suspend fun getPayload(request: PayloadRequest): PayloadResponse
+ suspend fun getInterface(): MyInterface
+ }
+ @PrivacySandboxInterface
+ interface MyInterface {
+ suspend fun getMorePayload(request: PayloadRequest): PayloadResponse
}
@PrivacySandboxValue
data class PayloadType(val size: Long, val appId: String)
@@ -96,6 +103,12 @@
isSuspend = false,
),
Method(
+ name = "getInterface",
+ parameters = listOf(),
+ returnType = Type("com.mysdk", "MyInterface"),
+ isSuspend = true,
+ ),
+ Method(
name = "getPayload",
parameters = listOf(
Parameter(
@@ -108,7 +121,23 @@
)
)
)
-
+ val expectedInterface =
+ AnnotatedInterface(
+ type = Type(packageName = "com.mysdk", simpleName = "MyInterface"),
+ methods = listOf(
+ Method(
+ name = "getMorePayload",
+ parameters = listOf(
+ Parameter(
+ "request",
+ expectedPayloadRequest.type
+ )
+ ),
+ returnType = expectedPayloadResponse.type,
+ isSuspend = true,
+ )
+ )
+ )
val expectedCallback = AnnotatedInterface(
type = Type(packageName = "com.mysdk", simpleName = "CustomCallback"),
methods = listOf(
@@ -134,6 +163,7 @@
expectedPayloadResponse,
)
assertThat(actualApi.callbacks).containsExactly(expectedCallback)
+ assertThat(actualApi.interfaces).containsExactly(expectedInterface)
}
@Test
@@ -335,7 +365,7 @@
}
private fun compileAndParseApi(vararg sources: Source): ParsedApi {
- compileIntoInterfaceDescriptorsJar(*sources)
- return ApiStubParser.parse(compileIntoInterfaceDescriptorsJar(*sources))
+ val classpath = mergedClasspath(assertCompiles(sources.toList()))
+ return ApiStubParser.parse(classpath)
}
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
index 2bc8ff1..c4be47f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/input/com/sdkwithcallbacks/SdkService.kt
@@ -3,10 +3,11 @@
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxCallback
import androidx.privacysandbox.tools.PrivacySandboxValue
+import androidx.privacysandbox.tools.PrivacySandboxInterface
@PrivacySandboxService
interface SdkService {
- suspend fun registerCallback(callback: SdkCallback)
+ fun registerCallback(callback: SdkCallback)
}
@PrivacySandboxCallback
@@ -16,7 +17,14 @@
fun onPrimitivesReceived(x: Int, y: Int)
fun onEmptyEvent()
+
+ fun onCompleteInterface(myInterface: MyInterface)
}
@PrivacySandboxValue
data class Response(val response: String)
+
+@PrivacySandboxInterface
+interface MyInterface {
+ fun doStuff()
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt
new file mode 100644
index 0000000..0474314
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterface.kt
@@ -0,0 +1,5 @@
+package com.sdkwithcallbacks
+
+public interface MyInterface {
+ public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..f325ee4
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdkwithcallbacks
+
+public class MyInterfaceClientProxy(
+ public val remote: IMyInterface,
+) : MyInterface {
+ public override fun doStuff(): Unit {
+ remote.doStuff()
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
index da06ec7..1e6c2d4 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallback.kt
@@ -1,6 +1,8 @@
package com.sdkwithcallbacks
public interface SdkCallback {
+ public fun onCompleteInterface(myInterface: MyInterface): Unit
+
public fun onEmptyEvent(): Unit
public fun onPrimitivesReceived(x: Int, y: Int): Unit
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
index a15d0c9..f4043ef 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkCallbackStubDelegate.kt
@@ -7,6 +7,10 @@
public class SdkCallbackStubDelegate internal constructor(
public val `delegate`: SdkCallback,
) : ISdkCallback.Stub() {
+ public override fun onCompleteInterface(myInterface: IMyInterface): Unit {
+ delegate.onCompleteInterface(MyInterfaceClientProxy(myInterface))
+ }
+
public override fun onEmptyEvent(): Unit {
delegate.onEmptyEvent()
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
index 8363e3d..510bd89 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkService.kt
@@ -1,5 +1,5 @@
package com.sdkwithcallbacks
public interface SdkService {
- public suspend fun registerCallback(callback: SdkCallback): Unit
+ public fun registerCallback(callback: SdkCallback): Unit
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
index 4ed15f2..83a8df2 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceClientProxy.kt
@@ -1,32 +1,9 @@
package com.sdkwithcallbacks
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
-
public class SdkServiceClientProxy(
- private val remote: ISdkService,
+ public val remote: ISdkService,
) : SdkService {
- public override suspend fun registerCallback(callback: SdkCallback): Unit =
- suspendCancellableCoroutine {
- var mCancellationSignal: ICancellationSignal? = null
- val transactionCallback = object: IUnitTransactionCallback.Stub() {
- override fun onCancellable(cancellationSignal: ICancellationSignal) {
- if (it.isCancelled) {
- cancellationSignal.cancel()
- }
- mCancellationSignal = cancellationSignal
- }
- override fun onSuccess() {
- it.resumeWith(Result.success(Unit))
- }
- override fun onFailure(errorCode: Int, errorMessage: String) {
- it.resumeWithException(RuntimeException(errorMessage))
- }
- }
- remote.registerCallback(SdkCallbackStubDelegate(callback), transactionCallback)
- it.invokeOnCancellation {
- mCancellationSignal?.cancel()
- }
+ public override fun registerCallback(callback: SdkCallback): Unit {
+ remote.registerCallback(SdkCallbackStubDelegate(callback))
}
}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
index 0936943..0a284a0 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/SdkServiceFactory.kt
@@ -1,24 +1,6 @@
package com.sdkwithcallbacks
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
-public suspend fun createSdkService(context: Context): SdkService = suspendCancellableCoroutine {
- val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
- val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
- override fun onResult(result: SandboxedSdk) {
- it.resume(SdkServiceClientProxy(ISdkService.Stub.asInterface(result.getInterface())))
- }
- override fun onError(error: LoadSdkException) {
- it.resumeWithException(error)
- }
- }
- sdkSandboxManager.loadSdk("com.sdkwithcallbacks", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToSdkService(binder: IBinder): SdkService =
+ SdkServiceClientProxy(ISdkService.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
new file mode 100644
index 0000000..fce47b8
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/input/com/sdk/MySdk.kt
@@ -0,0 +1,21 @@
+package com.sdk
+
+import androidx.privacysandbox.tools.PrivacySandboxInterface
+import androidx.privacysandbox.tools.PrivacySandboxService
+
+@PrivacySandboxService
+interface MySdk {
+ suspend fun getInterface(): MyInterface
+}
+
+@PrivacySandboxInterface
+interface MyInterface {
+ suspend fun add(x: Int, y: Int): Int
+
+ fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface)
+}
+
+@PrivacySandboxInterface
+interface MySecondInterface {
+ fun doStuff()
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
new file mode 100644
index 0000000..155dec9
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterface.kt
@@ -0,0 +1,7 @@
+package com.sdk
+
+public interface MyInterface {
+ public suspend fun add(x: Int, y: Int): Int
+
+ public fun doSomething(firstInterface: MyInterface, secondInterface: MySecondInterface): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..00b33dd
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MyInterfaceClientProxy.kt
@@ -0,0 +1,37 @@
+package com.sdk
+
+import com.sdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+public class MyInterfaceClientProxy(
+ public val remote: IMyInterface,
+) : MyInterface {
+ public override suspend fun add(x: Int, y: Int): Int = suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IIntTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: Int) {
+ it.resumeWith(Result.success(result))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.add(x, y, transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+
+ public override fun doSomething(firstInterface: MyInterface,
+ secondInterface: MySecondInterface): Unit {
+ remote.doSomething((firstInterface as MyInterfaceClientProxy).remote, (secondInterface as
+ MySecondInterfaceClientProxy).remote)
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt
new file mode 100644
index 0000000..967cd6d8
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdk.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+public interface MySdk {
+ public suspend fun getInterface(): MyInterface
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt
new file mode 100644
index 0000000..3a6ae61
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkClientProxy.kt
@@ -0,0 +1,31 @@
+package com.sdk
+
+import com.sdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+public class MySdkClientProxy(
+ public val remote: IMySdk,
+) : MySdk {
+ public override suspend fun getInterface(): MyInterface = suspendCancellableCoroutine {
+ var mCancellationSignal: ICancellationSignal? = null
+ val transactionCallback = object: IMyInterfaceTransactionCallback.Stub() {
+ override fun onCancellable(cancellationSignal: ICancellationSignal) {
+ if (it.isCancelled) {
+ cancellationSignal.cancel()
+ }
+ mCancellationSignal = cancellationSignal
+ }
+ override fun onSuccess(result: IMyInterface) {
+ it.resumeWith(Result.success(MyInterfaceClientProxy(result)))
+ }
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
+ }
+ }
+ remote.getInterface(transactionCallback)
+ it.invokeOnCancellation {
+ mCancellationSignal?.cancel()
+ }
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt
new file mode 100644
index 0000000..014a616
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySdkFactory.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+import android.os.IBinder
+
+public fun wrapToMySdk(binder: IBinder): MySdk = MySdkClientProxy(IMySdk.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
new file mode 100644
index 0000000..b858ed5
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterface.kt
@@ -0,0 +1,5 @@
+package com.sdk
+
+public interface MySecondInterface {
+ public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
new file mode 100644
index 0000000..67e7d28a
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/MySecondInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdk
+
+public class MySecondInterfaceClientProxy(
+ public val remote: IMySecondInterface,
+) : MySecondInterface {
+ public override fun doStuff(): Unit {
+ remote.doStuff()
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt
new file mode 100644
index 0000000..1e0a20d
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.sdk
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+ public override val message: String?,
+ public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..4b0857c
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/interfaces/output/com/sdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.sdk
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+ public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+ val exceptionClass = throwableParcel.exceptionClass
+ val errorMessage = throwableParcel.errorMessage
+ val stackTrace = throwableParcel.stackTrace
+ val exception = PrivacySandboxException(
+ "[$exceptionClass] $errorMessage",
+ throwableParcel.cause?.let { fromThrowableParcel(it) })
+ for (suppressed in throwableParcel.suppressedExceptions) {
+ exception.addSuppressed(fromThrowableParcel(suppressed))
+ }
+ exception.stackTrace =
+ stackTrace.map {
+ StackTraceElement(
+ it.declaringClass,
+ it.methodName,
+ it.fileName,
+ it.lineNumber
+ )
+ }.toTypedArray()
+ return exception
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt
new file mode 100644
index 0000000..d165932
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.mysdk
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+ public override val message: String?,
+ public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..42297ec
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.mysdk
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+ public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+ val exceptionClass = throwableParcel.exceptionClass
+ val errorMessage = throwableParcel.errorMessage
+ val stackTrace = throwableParcel.stackTrace
+ val exception = PrivacySandboxException(
+ "[$exceptionClass] $errorMessage",
+ throwableParcel.cause?.let { fromThrowableParcel(it) })
+ for (suppressed in throwableParcel.suppressedExceptions) {
+ exception.addSuppressed(fromThrowableParcel(suppressed))
+ }
+ exception.stackTrace =
+ stackTrace.map {
+ StackTraceElement(
+ it.declaringClass,
+ it.methodName,
+ it.fileName,
+ it.lineNumber
+ )
+ }.toTypedArray()
+ return exception
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
index 607b29e..2d5b21d 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkClientProxy.kt
@@ -1,11 +1,12 @@
package com.mysdk
-import kotlin.coroutines.resume
+import com.mysdk.PrivacySandboxThrowableParcelConverter
+import com.mysdk.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
public class TestSandboxSdkClientProxy(
- private val remote: ITestSandboxSdk,
+ public val remote: ITestSandboxSdk,
) : TestSandboxSdk {
public override suspend fun doSomethingAsync(
first: Int,
@@ -23,8 +24,8 @@
override fun onSuccess(result: Boolean) {
it.resumeWith(Result.success(result))
}
- override fun onFailure(errorCode: Int, errorMessage: String) {
- it.resumeWithException(RuntimeException(errorMessage))
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
}
}
remote.doSomethingAsync(first, second, third, transactionCallback)
@@ -77,8 +78,8 @@
override fun onSuccess() {
it.resumeWith(Result.success(Unit))
}
- override fun onFailure(errorCode: Int, errorMessage: String) {
- it.resumeWithException(RuntimeException(errorMessage))
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
}
}
remote.receiveAndReturnNothingAsync(transactionCallback)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
index 6bc1a04..db02846 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/primitives/output/com/mysdk/TestSandboxSdkFactory.kt
@@ -1,25 +1,6 @@
package com.mysdk
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
-public suspend fun createTestSandboxSdk(context: Context): TestSandboxSdk =
- suspendCancellableCoroutine {
- val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
- val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
- override fun onResult(result: SandboxedSdk) {
- it.resume(TestSandboxSdkClientProxy(ITestSandboxSdk.Stub.asInterface(result.getInterface())))
- }
- override fun onError(error: LoadSdkException) {
- it.resumeWithException(error)
- }
- }
- sdkSandboxManager.loadSdk("com.mysdk", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToTestSandboxSdk(binder: IBinder): TestSandboxSdk =
+ TestSandboxSdkClientProxy(ITestSandboxSdk.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
index 1fd19d0..6265564 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/input/com/sdkwithvalues/SdkInterface.kt
@@ -1,5 +1,6 @@
package com.sdkwithvalues
+import androidx.privacysandbox.tools.PrivacySandboxInterface
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
@@ -17,6 +18,7 @@
val message: String,
val floatingPoint: Float,
val hugeNumber: Double,
+ val myInterface: MyInterface,
)
@PrivacySandboxValue
@@ -24,3 +26,8 @@
@PrivacySandboxValue
data class SdkResponse(val success: Boolean, val originalRequest: SdkRequest)
+
+@PrivacySandboxInterface
+interface MyInterface {
+ fun doStuff()
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
index 295f659..dc6d855 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValue.kt
@@ -6,6 +6,7 @@
public val hugeNumber: Double,
public val id: Int,
public val message: String,
+ public val myInterface: MyInterface,
public val separator: Char,
public val shouldBeAwesome: Boolean,
)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
index 7bb55c5..ffa0b12 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/InnerSdkValueConverter.kt
@@ -8,6 +8,7 @@
hugeNumber = parcelable.hugeNumber,
id = parcelable.id,
message = parcelable.message,
+ myInterface = MyInterfaceClientProxy(parcelable.myInterface),
separator = parcelable.separator,
shouldBeAwesome = parcelable.shouldBeAwesome)
return annotatedValue
@@ -20,6 +21,7 @@
parcelable.hugeNumber = annotatedValue.hugeNumber
parcelable.id = annotatedValue.id
parcelable.message = annotatedValue.message
+ parcelable.myInterface = (annotatedValue.myInterface as MyInterfaceClientProxy).remote
parcelable.separator = annotatedValue.separator
parcelable.shouldBeAwesome = annotatedValue.shouldBeAwesome
return parcelable
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt
new file mode 100644
index 0000000..b7db6ef
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterface.kt
@@ -0,0 +1,5 @@
+package com.sdkwithvalues
+
+public interface MyInterface {
+ public fun doStuff(): Unit
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt
new file mode 100644
index 0000000..2fd68e4
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/MyInterfaceClientProxy.kt
@@ -0,0 +1,9 @@
+package com.sdkwithvalues
+
+public class MyInterfaceClientProxy(
+ public val remote: IMyInterface,
+) : MyInterface {
+ public override fun doStuff(): Unit {
+ remote.doStuff()
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt
new file mode 100644
index 0000000..7597726
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxException.kt
@@ -0,0 +1,8 @@
+package com.sdkwithvalues
+
+import java.lang.RuntimeException
+
+public class PrivacySandboxException(
+ public override val message: String?,
+ public override val cause: Throwable?,
+) : RuntimeException()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt
new file mode 100644
index 0000000..34de1d7
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/PrivacySandboxThrowableParcelConverter.kt
@@ -0,0 +1,27 @@
+package com.sdkwithvalues
+
+import java.lang.StackTraceElement
+
+public object PrivacySandboxThrowableParcelConverter {
+ public fun fromThrowableParcel(throwableParcel: PrivacySandboxThrowableParcel): Throwable {
+ val exceptionClass = throwableParcel.exceptionClass
+ val errorMessage = throwableParcel.errorMessage
+ val stackTrace = throwableParcel.stackTrace
+ val exception = PrivacySandboxException(
+ "[$exceptionClass] $errorMessage",
+ throwableParcel.cause?.let { fromThrowableParcel(it) })
+ for (suppressed in throwableParcel.suppressedExceptions) {
+ exception.addSuppressed(fromThrowableParcel(suppressed))
+ }
+ exception.stackTrace =
+ stackTrace.map {
+ StackTraceElement(
+ it.declaringClass,
+ it.methodName,
+ it.fileName,
+ it.lineNumber
+ )
+ }.toTypedArray()
+ return exception
+ }
+}
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
index 660db1b..1d6fada 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceClientProxy.kt
@@ -1,13 +1,13 @@
package com.sdkwithvalues
+import com.sdkwithvalues.PrivacySandboxThrowableParcelConverter.fromThrowableParcel
import com.sdkwithvalues.SdkRequestConverter.toParcelable
import com.sdkwithvalues.SdkResponseConverter.fromParcelable
-import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.suspendCancellableCoroutine
public class SdkInterfaceClientProxy(
- private val remote: ISdkInterface,
+ public val remote: ISdkInterface,
) : SdkInterface {
public override suspend fun exampleMethod(request: SdkRequest): SdkResponse =
suspendCancellableCoroutine {
@@ -22,8 +22,8 @@
override fun onSuccess(result: ParcelableSdkResponse) {
it.resumeWith(Result.success(fromParcelable(result)))
}
- override fun onFailure(errorCode: Int, errorMessage: String) {
- it.resumeWithException(RuntimeException(errorMessage))
+ override fun onFailure(throwableParcel: PrivacySandboxThrowableParcel) {
+ it.resumeWithException(fromThrowableParcel(throwableParcel))
}
}
remote.exampleMethod(toParcelable(request), transactionCallback)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
index bc79005..6a84537 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/SdkInterfaceFactory.kt
@@ -1,25 +1,6 @@
package com.sdkwithvalues
-import android.app.sdksandbox.LoadSdkException
-import android.app.sdksandbox.SandboxedSdk
-import android.app.sdksandbox.SdkSandboxManager
-import android.content.Context
-import android.os.Bundle
-import android.os.OutcomeReceiver
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlinx.coroutines.suspendCancellableCoroutine
+import android.os.IBinder
-public suspend fun createSdkInterface(context: Context): SdkInterface =
- suspendCancellableCoroutine {
- val sdkSandboxManager = context.getSystemService(SdkSandboxManager::class.java)
- val outcomeReceiver = object: OutcomeReceiver<SandboxedSdk, LoadSdkException> {
- override fun onResult(result: SandboxedSdk) {
- it.resume(SdkInterfaceClientProxy(ISdkInterface.Stub.asInterface(result.getInterface())))
- }
- override fun onError(error: LoadSdkException) {
- it.resumeWithException(error)
- }
- }
- sdkSandboxManager.loadSdk("com.sdkwithvalues", Bundle.EMPTY, Runnable::run, outcomeReceiver)
-}
+public fun wrapToSdkInterface(binder: IBinder): SdkInterface =
+ SdkInterfaceClientProxy(ISdkInterface.Stub.asInterface(binder))
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
index 4bcd717..812d48a 100644
--- a/privacysandbox/tools/tools-apipackager/build.gradle
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -28,6 +28,7 @@
implementation(libs.asmCommons)
implementation project(path: ':privacysandbox:tools:tools')
+ implementation project(path: ':privacysandbox:tools:tools-core')
testImplementation(project(":internal-testutils-truth"))
testImplementation(project(":privacysandbox:tools:tools-testing"))
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
index 0a1ad9f..5d8437f 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
@@ -17,9 +17,11 @@
package androidx.privacysandbox.tools.apipackager
import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxInterface
import androidx.privacysandbox.tools.PrivacySandboxService
import androidx.privacysandbox.tools.PrivacySandboxValue
-import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.readBytes
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
@@ -29,11 +31,12 @@
internal object AnnotationInspector {
private val privacySandboxAnnotations = setOf(
PrivacySandboxCallback::class,
+ PrivacySandboxInterface::class,
PrivacySandboxService::class,
PrivacySandboxValue::class,
)
- fun hasPrivacySandboxAnnotation(classFile: File): Boolean {
+ fun hasPrivacySandboxAnnotation(classFile: Path): Boolean {
val reader = ClassReader(classFile.readBytes())
val annotationExtractor = AnnotationExtractor()
reader.accept(
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
index cb5f049..382927c 100644
--- a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
@@ -23,6 +23,9 @@
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.notExists
+import androidx.privacysandbox.tools.core.Metadata
+import kotlin.io.path.extension
+import kotlin.io.path.inputStream
class PrivacySandboxApiPackager {
@@ -55,14 +58,24 @@
ZipOutputStream(outputFile.outputStream()).use { zipOutputStream ->
sdkClasspath.toFile().walk()
- .filter { it.extension == "class" }
- .filter { hasPrivacySandboxAnnotation(it) }
- .forEach { classFile ->
- val zipEntry = ZipEntry(sdkClasspath.relativize(classFile.toPath()).toString())
+ .map { it.toPath() }
+ .filter { shouldKeepFile(sdkClasspath, it) }
+ .forEach { file ->
+ val zipEntry = ZipEntry(sdkClasspath.relativize(file).toString())
zipOutputStream.putNextEntry(zipEntry)
- classFile.inputStream().copyTo(zipOutputStream)
+ file.inputStream().copyTo(zipOutputStream)
zipOutputStream.closeEntry()
}
}
}
+
+ private fun shouldKeepFile(sdkClasspath: Path, filePath: Path): Boolean {
+ if (sdkClasspath.relativize(filePath) == Metadata.filePath) {
+ return true
+ }
+ if (filePath.extension == "class" && hasPrivacySandboxAnnotation(filePath)) {
+ return true
+ }
+ return false
+ }
}
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
index 5469653..10a9cad 100644
--- a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -16,6 +16,8 @@
package androidx.privacysandbox.tools.apipackager
+import androidx.privacysandbox.tools.core.Metadata
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
import androidx.room.compiler.processing.util.Source
@@ -26,7 +28,10 @@
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipInputStream
+import kotlin.io.path.createDirectories
import kotlin.io.path.createFile
+import kotlin.io.path.inputStream
+import kotlin.io.path.outputStream
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -116,6 +121,38 @@
)
assertThat(compileWithExtraClasspath(appSource, packagedSdkClasspath)).succeeds()
}
+ @Test
+ fun sdkClasspathWithMetadataFile_isKeptInDescriptor() {
+ val metadata = ToolMetadata.newBuilder()
+ .setCodeGenerationVersion(42)
+ .build()
+ val source = Source.kotlin(
+ "com/mysdk/Valid.kt", """
+ |package com.mysdk
+ |interface Valid
+ """.trimMargin()
+ )
+ val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+ .outputClasspath.first().toPath()
+
+ val metadataPath = sdkClasspath.resolve(Metadata.filePath).also {
+ it.parent.createDirectories()
+ it.createFile()
+ }
+ metadata.writeTo(metadataPath.outputStream())
+
+ val sdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+ PrivacySandboxApiPackager().packageSdkDescriptors(sdkClasspath, sdkDescriptor)
+ val packagedMetadata =
+ ZipInputStream(sdkDescriptor.inputStream()).use { input ->
+ generateSequence { input.nextEntry }
+ .filter { it.name == Metadata.filePath.toString() }
+ .map { ToolMetadata.parseFrom(input.readAllBytes()) }
+ .toList()
+ }
+
+ assertThat(packagedMetadata).containsExactly(metadata)
+ }
@Test
fun sdkClasspathDoesNotExist_throwException() {
@@ -162,9 +199,9 @@
}
}
- /** Compiles the given source files and returns a classpath with the results. */
- private fun compileAndReturnUnzippedPackagedClasspath(vararg sources: Source): File {
- val result = compileAll(sources.toList(), includePrivacySandboxPlatformSources = false)
+ /** Compiles the given source file and returns a classpath with the results. */
+ private fun compileAndReturnUnzippedPackagedClasspath(source: Source): File {
+ val result = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
assertThat(result).succeeds()
assertThat(result.outputClasspath).hasSize(1)
@@ -173,7 +210,7 @@
PrivacySandboxApiPackager().packageSdkDescriptors(originalClasspath, descriptors)
val outputDir = makeTestDirectory().toFile()
- ZipInputStream(descriptors.toFile().inputStream()).use { input ->
+ ZipInputStream(descriptors.inputStream()).use { input ->
generateSequence { input.nextEntry }
.forEach {
val file: File = outputDir.resolve(it.name)
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index af7b5c3..f18b9ba 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -22,11 +22,13 @@
plugins {
id("AndroidXPlugin")
id("kotlin")
+ id("com.google.protobuf")
}
dependencies {
api(libs.kotlinStdlib)
- implementation(libs.kotlinPoet)
+ api(libs.protobufLite)
+ api(libs.kotlinPoet)
testImplementation(libs.junit)
testImplementation(libs.truth)
@@ -49,6 +51,21 @@
}
}
+protobuf {
+ protoc {
+ artifact = libs.protobufCompiler.get()
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ java {
+ option "lite"
+ }
+ }
+ }
+ }
+}
+
androidx {
name = "androidx.privacysandbox.tools:tools-core"
type = LibraryType.ANNOTATION_PROCESSOR_UTILS
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt
new file mode 100644
index 0000000..06a088a
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/Metadata.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core
+
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
+import kotlin.io.path.Path
+
+/** Privacy Sandbox Tool metadata constants. */
+object Metadata {
+ /** Tool metadata message. It's serialized and stored in every SDK API descriptor. */
+ val toolMetadata: ToolMetadata =
+ ToolMetadata.newBuilder()
+ .setCodeGenerationVersion(1)
+ .build()
+
+ /** Relative path to metadata file in SDK API descriptor jar. */
+ val filePath = Path("META-INF/privacysandbox/tool-metadata.pb")
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
index d61b8e8..4c245ac 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/AidlGenerator.kt
@@ -20,7 +20,7 @@
import androidx.privacysandbox.tools.core.generator.poet.AidlInterfaceSpec
import androidx.privacysandbox.tools.core.generator.poet.AidlInterfaceSpec.Companion.aidlInterface
import androidx.privacysandbox.tools.core.generator.poet.AidlMethodSpec
-import androidx.privacysandbox.tools.core.generator.poet.AidlParcelableSpec
+import androidx.privacysandbox.tools.core.generator.poet.AidlParcelableSpec.Companion.aidlParcelable
import androidx.privacysandbox.tools.core.generator.poet.AidlTypeSpec
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.AnnotatedValue
@@ -30,6 +30,7 @@
import androidx.privacysandbox.tools.core.model.Type
import androidx.privacysandbox.tools.core.model.Types
import androidx.privacysandbox.tools.core.model.getOnlyService
+import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
@@ -53,6 +54,8 @@
}
const val cancellationSignalName = "ICancellationSignal"
+ const val throwableParcelName = "PrivacySandboxThrowableParcel"
+ const val parcelableStackFrameName = "ParcelableStackFrame"
}
private fun generate(): List<GeneratedSource> {
@@ -91,12 +94,11 @@
private fun generateAidlContent(): List<AidlFileSpec> {
val values = api.values.map(::generateValue)
- val transactionCallbacks = generateTransactionCallbacks()
val service = aidlInterface(api.getOnlyService())
val customCallbacks = api.callbacks.map(::aidlInterface)
val interfaces = api.interfaces.map(::aidlInterface)
- return transactionCallbacks +
- generateICancellationSignal() +
+ val suspendFunctionUtilities = generateSuspendFunctionUtilities()
+ return suspendFunctionUtilities +
service +
values +
customCallbacks +
@@ -128,6 +130,14 @@
)
}
+ private fun generateSuspendFunctionUtilities(): List<AidlFileSpec> {
+ if (!api.hasSuspendFunctions()) return emptyList()
+ return generateTransactionCallbacks() +
+ generateParcelableFailure() +
+ generateParcelableStackTrace() +
+ generateICancellationSignal()
+ }
+
private fun generateTransactionCallbacks(): List<AidlFileSpec> {
val annotatedInterfaces = api.services + api.interfaces
return annotatedInterfaces
@@ -146,8 +156,7 @@
if (type != Types.unit) addParameter(Parameter("result", type))
}
addMethod("onFailure") {
- addParameter("errorCode", primitive("int"))
- addParameter("errorMessage", primitive("String"))
+ addParameter("throwableParcel", AidlTypeSpec(throwableParcelType()), isIn = true)
}
}
}
@@ -156,14 +165,33 @@
addMethod("cancel")
}
+ private fun generateParcelableFailure(): AidlFileSpec {
+ return aidlParcelable(throwableParcelType()) {
+ addProperty("exceptionClass", primitive("String"))
+ addProperty("errorMessage", primitive("String"))
+ addProperty("stackTrace", AidlTypeSpec(parcelableStackFrameType(), isList = true))
+ addProperty("cause", AidlTypeSpec(throwableParcelType()), isNullable = true)
+ addProperty(
+ "suppressedExceptions", AidlTypeSpec(throwableParcelType(), isList = true)
+ )
+ }
+ }
+
+ private fun generateParcelableStackTrace(): AidlFileSpec {
+ return aidlParcelable(parcelableStackFrameType()) {
+ addProperty("declaringClass", primitive("String"))
+ addProperty("methodName", primitive("String"))
+ addProperty("fileName", primitive("String"))
+ addProperty("lineNumber", primitive("int"))
+ }
+ }
+
private fun generateValue(value: AnnotatedValue): AidlFileSpec {
- val typesToImport =
- value.properties.mapNotNull { api.valueMap[it.type]?.aidlType()?.innerType }.toSet()
- return AidlParcelableSpec(type = value.aidlType().innerType,
- typesToImport = typesToImport,
- properties = value.properties.map {
- "${getAidlTypeDeclaration(it.type)} ${it.name};"
- })
+ return aidlParcelable(value.aidlType().innerType) {
+ for (property in value.properties) {
+ addProperty(property.name, getAidlTypeDeclaration(property.type))
+ }
+ }
}
private fun getAidlFile(rootPath: Path, aidlSource: AidlFileSpec) = Paths.get(
@@ -181,6 +209,8 @@
private fun packageName() = api.getOnlyService().type.packageName
private fun cancellationSignalType() = AidlTypeSpec(Type(packageName(), cancellationSignalName))
+ private fun throwableParcelType() = Type(packageName(), throwableParcelName)
+ private fun parcelableStackFrameType() = Type(packageName(), parcelableStackFrameName)
private fun transactionCallback(type: Type) =
AidlTypeSpec(Type(api.getOnlyService().type.packageName, type.transactionCallbackName()))
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
index 1b0e9f8..c791c9c 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverter.kt
@@ -16,7 +16,7 @@
package androidx.privacysandbox.tools.core.generator
-import androidx.privacysandbox.tools.core.model.Parameter
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.Type
import androidx.privacysandbox.tools.core.model.Types
@@ -24,8 +24,7 @@
import com.squareup.kotlinpoet.CodeBlock
/** Utility to generate [CodeBlock]s that convert values to/from their binder equivalent. */
-class BinderCodeConverter(private val api: ParsedApi) {
-
+abstract class BinderCodeConverter(private val api: ParsedApi) {
/**
* Generate a block that converts the given expression from the binder representation to its
* model equivalent.
@@ -47,20 +46,15 @@
}
val sandboxInterface = api.interfaceMap[type]
if (sandboxInterface != null) {
- return CodeBlock.of(
- "(%L as %T).delegate", expression, sandboxInterface.stubDelegateNameSpec())
+ return convertToInterfaceModelCode(sandboxInterface, expression)
}
return CodeBlock.of(expression)
}
- /**
- * Convert the given [Parameter] from the binder representation to its model equivalent.
- *
- * See [convertToModelCode].
- */
- fun convertToModelCode(parameter: Parameter): CodeBlock {
- return convertToModelCode(parameter.type, parameter.name)
- }
+ protected abstract fun convertToInterfaceModelCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock
/**
* Generate a block that converts the given expression from the model representation to its
@@ -83,19 +77,15 @@
}
val sandboxInterface = api.interfaceMap[type]
if (sandboxInterface != null) {
- return CodeBlock.of("%T(%L)", sandboxInterface.stubDelegateNameSpec(), expression)
+ return convertToInterfaceBinderCode(sandboxInterface, expression)
}
return CodeBlock.of(expression)
}
- /**
- * Convert the given [Parameter] from the model representation to its binder equivalent.
- *
- * See [convertToBinderCode].
- */
- fun convertToBinderCode(parameter: Parameter): CodeBlock {
- return convertToBinderCode(parameter.type, parameter.name)
- }
+ protected abstract fun convertToInterfaceBinderCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock
/** Convert the given model type declaration to its binder equivalent. */
fun convertToBinderType(type: Type): ClassName {
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt
new file mode 100644
index 0000000..679197d
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import com.squareup.kotlinpoet.CodeBlock
+
+class ClientBinderCodeConverter(api: ParsedApi) : BinderCodeConverter(api) {
+ override fun convertToInterfaceModelCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock = CodeBlock.of("%T(%L)", annotatedInterface.clientProxyNameSpec(), expression)
+
+ override fun convertToInterfaceBinderCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock =
+ CodeBlock.of(
+ "(%L as %T).remote", expression, annotatedInterface.clientProxyNameSpec()
+ )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
index 49b17ce..97472c9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ClientProxyTypeGenerator.kt
@@ -16,6 +16,9 @@
package androidx.privacysandbox.tools.core.generator
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.throwableParcelName
+import androidx.privacysandbox.tools.core.generator.SpecNames.resumeWithExceptionMethod
+import androidx.privacysandbox.tools.core.generator.SpecNames.suspendCancellableCoroutineMethod
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import com.squareup.kotlinpoet.ClassName
@@ -23,7 +26,6 @@
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
-import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.joinToCode
@@ -45,7 +47,7 @@
primaryConstructor(
listOf(
PropertySpec.builder("remote", remoteBinderClassName)
- .addModifiers(KModifier.PRIVATE).build()
+ .addModifiers(KModifier.PUBLIC).build()
)
)
@@ -54,12 +56,6 @@
return FileSpec.builder(annotatedInterface.type.packageName, className).build {
addCommonSettings()
-
- // TODO(b/254660742): Only add these when needed
- addImport("kotlinx.coroutines", "suspendCancellableCoroutine")
- addImport("kotlin.coroutines", "resume")
- addImport("kotlin.coroutines", "resumeWithException")
-
addType(classSpec)
}
}
@@ -77,7 +73,7 @@
returns(method.returnType.poetSpec())
addCode {
- addControlFlow("return suspendCancellableCoroutine") {
+ addControlFlow("return %M", suspendCancellableCoroutineMethod) {
addStatement("var mCancellationSignal: %T? = null", cancellationSignalClassName)
add(generateTransactionCallbackObject(method))
@@ -118,10 +114,16 @@
add(generateTransactionCallbackOnSuccess(method))
- addControlFlow("override fun onFailure(errorCode: Int, errorMessage: String)") {
+ addControlFlow(
+ "override fun onFailure(throwableParcel: %T)",
+ ClassName(basePackageName, throwableParcelName)
+ ) {
addStatement(
- "it.%M(RuntimeException(errorMessage))",
- MemberName("kotlin.coroutines", "resumeWithException")
+ "it.%M(%M(throwableParcel))",
+ resumeWithExceptionMethod,
+ ThrowableParcelConverterFileGenerator.fromThrowableParcelNameSpec(
+ basePackageName
+ )
)
}
}
@@ -154,7 +156,8 @@
extraParameters: List<CodeBlock> = emptyList(),
) = CodeBlock.builder().build {
val parameters =
- method.parameters.map { binderCodeConverter.convertToBinderCode(it) } + extraParameters
+ method.parameters.map { binderCodeConverter.convertToBinderCode(it.type, it.name) } +
+ extraParameters
addStatement {
add("remote.${method.name}(")
add(parameters.joinToCode())
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
index 828123af..bb26bbd 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/KotlinPoetSpecs.kt
@@ -41,16 +41,22 @@
fun AnnotatedValue.converterNameSpec() =
ClassName(type.packageName, "${type.simpleName}Converter")
+
fun AnnotatedValue.toParcelableNameSpec() =
MemberName(converterNameSpec(), "toParcelable")
+
fun AnnotatedValue.fromParcelableNameSpec() =
MemberName(converterNameSpec(), "fromParcelable")
+
fun AnnotatedValue.parcelableNameSpec() =
ClassName(type.packageName, "Parcelable${type.simpleName}")
+
fun AnnotatedInterface.clientProxyNameSpec() =
ClassName(type.packageName, "${type.simpleName}ClientProxy")
+
fun AnnotatedInterface.stubDelegateNameSpec() =
ClassName(type.packageName, "${type.simpleName}StubDelegate")
+
fun AnnotatedInterface.aidlInterfaceNameSpec() =
ClassName(type.packageName, aidlType().innerType.simpleName)
@@ -131,11 +137,15 @@
add("\n»")
}
-fun CodeBlock.Companion.statement(builderBlock: CodeBlock.Builder.() -> Unit) =
- builder().build { addStatement(builderBlock) }
-
object SpecNames {
val dispatchersMainClass = ClassName("kotlinx.coroutines", "Dispatchers", "Main")
+ val delicateCoroutinesApiClass = ClassName("kotlinx.coroutines", "DelicateCoroutinesApi")
val globalScopeClass = ClassName("kotlinx.coroutines", "GlobalScope")
+ val stackTraceElementClass = ClassName("java.lang", "StackTraceElement")
+ val suspendCancellableCoroutineMethod =
+ MemberName("kotlinx.coroutines", "suspendCancellableCoroutine", isExtension = true)
+ val resumeWithExceptionMethod =
+ MemberName("kotlin.coroutines", "resumeWithException", isExtension = true)
val launchMethod = MemberName("kotlinx.coroutines", "launch", isExtension = true)
+ val iBinderClassName = ClassName("android.os", "IBinder")
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt
new file mode 100644
index 0000000..f587725
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/PrivacySandboxExceptionFileGenerator.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asTypeName
+
+class PrivacySandboxExceptionFileGenerator(private val basePackageName: String) {
+ companion object {
+ const val privacySandboxExceptionName = "PrivacySandboxException"
+ }
+
+ fun generate(): FileSpec {
+ val classSpec = TypeSpec.classBuilder(privacySandboxExceptionName).build {
+ superclass(RuntimeException::class)
+ addModifiers(KModifier.PUBLIC)
+ primaryConstructor(
+ listOf(
+ PropertySpec.builder(
+ "message",
+ String::class.asTypeName().copy(nullable = true),
+ ).addModifiers(KModifier.OVERRIDE).build(),
+ PropertySpec.builder(
+ "cause",
+ Throwable::class.asTypeName().copy(nullable = true),
+ ).addModifiers(KModifier.OVERRIDE).build()
+ )
+ )
+ }
+
+ return FileSpec.builder(
+ basePackageName,
+ privacySandboxExceptionName
+ ).build {
+ addCommonSettings()
+ addType(classSpec)
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt
new file mode 100644
index 0000000..e76dd5fb
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverter.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.model.AnnotatedInterface
+import androidx.privacysandbox.tools.core.model.ParsedApi
+import com.squareup.kotlinpoet.CodeBlock
+
+class ServerBinderCodeConverter(private val api: ParsedApi) : BinderCodeConverter(api) {
+ override fun convertToInterfaceModelCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock = CodeBlock.of(
+ "(%L as %T).delegate", expression, annotatedInterface.stubDelegateNameSpec()
+ )
+
+ override fun convertToInterfaceBinderCode(
+ annotatedInterface: AnnotatedInterface,
+ expression: String
+ ): CodeBlock =
+ CodeBlock.of("%T(%L)", annotatedInterface.stubDelegateNameSpec(), expression)
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
index 649485a..e94ab33 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/StubDelegatesGenerator.kt
@@ -16,6 +16,7 @@
package androidx.privacysandbox.tools.core.generator
+import androidx.privacysandbox.tools.core.generator.SpecNames.delicateCoroutinesApiClass
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.Types
@@ -70,6 +71,7 @@
addModifiers(KModifier.OVERRIDE)
addParameters(getParameters(method))
addCode {
+ addStatement("@OptIn(%T::class)", delicateCoroutinesApiClass)
addControlFlow(
"val job = %T.%M(%T)",
SpecNames.globalScopeClass,
@@ -78,7 +80,9 @@
) {
addControlFlow("try") {
addStatement {
- add("val result = ")
+ if (method.returnType != Types.unit) {
+ add("val result = ")
+ }
add(getDelegateCallBlock(method))
}
if (method.returnType == Types.unit) {
@@ -86,12 +90,19 @@
} else {
addStatement(
"transactionCallback.onSuccess(%L)",
- binderCodeConverter.convertToBinderCode(method.returnType, "result")
+ binderCodeConverter.convertToBinderCode(
+ method.returnType, "result"
+ )
)
}
}
addControlFlow("catch (t: Throwable)") {
- addStatement("transactionCallback.onFailure(404, t.message)")
+ addStatement(
+ "transactionCallback.onFailure(%M(t))",
+ ThrowableParcelConverterFileGenerator.toThrowableParcelNameSpec(
+ basePackageName
+ )
+ )
}
}
addStatement(
@@ -127,7 +138,8 @@
private fun getDelegateCallBlock(method: Method) = CodeBlock.builder().build {
add("delegate.${method.name}(")
- add(method.parameters.map { binderCodeConverter.convertToModelCode(it) }.joinToCode())
+ add(method.parameters.map { binderCodeConverter.convertToModelCode(it.type, it.name) }
+ .joinToCode())
add(")")
}
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt
new file mode 100644
index 0000000..4ac7844
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ThrowableParcelConverterFileGenerator.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.core.generator
+
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.parcelableStackFrameName
+import androidx.privacysandbox.tools.core.generator.AidlGenerator.Companion.throwableParcelName
+import androidx.privacysandbox.tools.core.generator.SpecNames.stackTraceElementClass
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.MemberName
+import com.squareup.kotlinpoet.TypeSpec
+
+class ThrowableParcelConverterFileGenerator(private val basePackageName: String) {
+ companion object {
+ const val converterName = "${throwableParcelName}Converter"
+ fun toThrowableParcelNameSpec(packageName: String) = MemberName(ClassName(
+ packageName,
+ converterName
+ ), "toThrowableParcel")
+ fun fromThrowableParcelNameSpec(packageName: String) = MemberName(ClassName(
+ packageName,
+ converterName
+ ), "fromThrowableParcel")
+ }
+
+ private val throwableParcelNameSpec = ClassName(basePackageName, throwableParcelName)
+ private val parcelableStackFrameNameSpec = ClassName(basePackageName, parcelableStackFrameName)
+ private val toThrowableParcelNameSpec = toThrowableParcelNameSpec(basePackageName)
+ private val fromThrowableParcelNameSpec = fromThrowableParcelNameSpec(basePackageName)
+
+ fun generate(convertToParcel: Boolean = false, convertFromParcel: Boolean = false) =
+ FileSpec.builder(
+ basePackageName,
+ converterName
+ ).build {
+ addCommonSettings()
+ addType(generateConverter(convertToParcel, convertFromParcel))
+ }
+
+ private fun generateConverter(convertToParcel: Boolean, convertFromParcel: Boolean) =
+ TypeSpec.objectBuilder(ClassName(basePackageName, converterName)).build {
+ if (convertToParcel) addFunction(generateToThrowableParcel())
+ if (convertFromParcel) addFunction(generateFromThrowableParcel())
+ }
+
+ private fun generateToThrowableParcel() =
+ FunSpec.builder(toThrowableParcelNameSpec.simpleName).build {
+ addParameter("throwable", Throwable::class)
+ returns(throwableParcelNameSpec)
+ addCode {
+ add(
+ """
+ val parcel = %T()
+ parcel.exceptionClass = throwable::class.qualifiedName
+ parcel.errorMessage = throwable.message
+ parcel.stackTrace = throwable.stackTrace.map {
+ val stackFrame = %T()
+ stackFrame.declaringClass = it.className
+ stackFrame.methodName = it.methodName
+ stackFrame.fileName = it.fileName
+ stackFrame.lineNumber = it.lineNumber
+ stackFrame
+ }.toTypedArray()
+ throwable.cause?.let {
+ parcel.cause = ${toThrowableParcelNameSpec.simpleName}(it)
+ }
+ parcel.suppressedExceptions =
+ throwable.suppressedExceptions.map {
+ ${toThrowableParcelNameSpec.simpleName}(it)
+ }.toTypedArray()
+ return parcel
+ """.trimIndent(),
+ throwableParcelNameSpec,
+ parcelableStackFrameNameSpec,
+ )
+ }
+ }
+
+ private fun generateFromThrowableParcel() =
+ FunSpec.builder(fromThrowableParcelNameSpec.simpleName).build {
+ addParameter("throwableParcel", throwableParcelNameSpec)
+ returns(Throwable::class)
+ addCode {
+ add(
+ """
+ val exceptionClass = throwableParcel.exceptionClass
+ val errorMessage = throwableParcel.errorMessage
+ val stackTrace = throwableParcel.stackTrace
+ val exception = PrivacySandboxException(
+ "[${'$'}exceptionClass] ${'$'}errorMessage",
+ throwableParcel.cause?.let { ${fromThrowableParcelNameSpec.simpleName}(it) })
+ for (suppressed in throwableParcel.suppressedExceptions) {
+ exception.addSuppressed(${fromThrowableParcelNameSpec.simpleName}(suppressed))
+ }
+ exception.stackTrace =
+ stackTrace.map {
+ %T(
+ it.declaringClass,
+ it.methodName,
+ it.fileName,
+ it.lineNumber
+ )
+ }.toTypedArray()
+ return exception
+ """.trimIndent(),
+ stackTraceElementClass,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
index 0687c63..0b5ced9 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
@@ -17,7 +17,6 @@
package androidx.privacysandbox.tools.core.generator
import androidx.privacysandbox.tools.core.model.AnnotatedValue
-import androidx.privacysandbox.tools.core.model.ParsedApi
import androidx.privacysandbox.tools.core.model.ValueProperty
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
@@ -29,7 +28,7 @@
* Generates a file that defines a converter for an SDK defined value and its AIDL parcelable
* counterpart.
*/
-class ValueConverterFileGenerator(private val api: ParsedApi) {
+class ValueConverterFileGenerator(private val binderConverter: BinderCodeConverter) {
fun generate(value: AnnotatedValue) =
FileSpec.builder(
@@ -42,9 +41,9 @@
private fun generateConverter(value: AnnotatedValue) =
TypeSpec.objectBuilder(value.converterNameSpec()).build {
- addFunction(generateFromParcelable(value))
- addFunction(generateToParcelable(value))
- }
+ addFunction(generateFromParcelable(value))
+ addFunction(generateToParcelable(value))
+ }
private fun generateToParcelable(value: AnnotatedValue) =
FunSpec.builder(value.toParcelableNameSpec().simpleName).build {
@@ -57,14 +56,13 @@
private fun generateToParcelablePropertyConversion(property: ValueProperty) =
CodeBlock.builder().build {
- val innerValue = api.valueMap[property.type]
- if (innerValue != null) {
- addStatement(
- "parcelable.${property.name} = %M(annotatedValue.${property.name})",
- innerValue.toParcelableNameSpec())
- } else {
- addStatement("parcelable.${property.name} = annotatedValue.${property.name}")
- }
+ addStatement(
+ "parcelable.${property.name} = %L",
+ binderConverter.convertToBinderCode(
+ property.type,
+ "annotatedValue.${property.name}"
+ )
+ )
}
private fun generateFromParcelable(value: AnnotatedValue) =
@@ -82,14 +80,11 @@
private fun generateFromParcelablePropertyConversion(property: ValueProperty) =
CodeBlock.builder().build {
- val innerValue = api.valueMap[property.type]
- if (innerValue != null) {
- add(
- "${property.name} = %M(parcelable.${property.name})",
- innerValue.fromParcelableNameSpec()
+ add(
+ "${property.name} = %L",
+ binderConverter.convertToModelCode(
+ property.type, "parcelable.${property.name}"
)
- } else {
- add("${property.name} = parcelable.${property.name}")
- }
+ )
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
index 81ced87..3453725 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlInterfaceSpec.kt
@@ -35,7 +35,8 @@
override val typesToImport: Set<Type>
get() {
return methods.flatMap { method ->
- method.parameters.map { it.type }.filter { it.requiresImport }.map { it.innerType }
+ method.parameters.map { it.type }
+ .filter { it.requiresImport && it.innerType != type }.map { it.innerType }
}.toSet()
}
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
index 894c7a3..39fb6a6 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParameterSpec.kt
@@ -21,10 +21,6 @@
val type: AidlTypeSpec,
val isIn: Boolean = false
) {
- companion object {
- fun parameter(name: String, vararg parameters: AidlParameterSpec) =
- AidlMethodSpec(name, parameters.toList())
- }
override fun toString() = "${if (isIn) "in " else ""}$type $name"
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
index 9c7cf46..94906d5 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlParcelableSpec.kt
@@ -21,16 +21,45 @@
/** AIDL file with a single parcelable defined. */
internal data class AidlParcelableSpec(
override val type: Type,
- val properties: List<String>,
- override val typesToImport: Set<Type> = emptySet(),
+ val properties: List<AidlPropertySpec>,
) : AidlFileSpec {
+ companion object {
+ fun aidlParcelable(
+ type: Type,
+ block: Builder.() -> Unit = {}
+ ): AidlParcelableSpec {
+ return Builder(type).also(block).build()
+ }
+ }
+
+ override val typesToImport: Set<Type>
+ get() {
+ return properties.map { it.type }.filter { it.requiresImport && it.innerType != type }
+ .map { it.innerType }
+ .toSet()
+ }
+
override val innerContent: String
get() {
- val body = properties.sorted().joinToString("\n| ")
+ val body = properties.map { it.toString() }.sorted().joinToString("\n| ")
return """
|parcelable ${type.simpleName} {
| $body
|}
""".trimMargin()
}
+
+ class Builder(val type: Type) {
+ val properties = mutableListOf<AidlPropertySpec>()
+
+ fun addProperty(property: AidlPropertySpec) {
+ properties.add(property)
+ }
+
+ fun addProperty(name: String, type: AidlTypeSpec, isNullable: Boolean = false) {
+ addProperty(AidlPropertySpec(name, type, isNullable))
+ }
+
+ fun build() = AidlParcelableSpec(type, properties)
+ }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
similarity index 67%
copy from privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
copy to privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
index 79122bf..b78ad94 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/KotlinPoetUtils.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlPropertySpec.kt
@@ -14,10 +14,12 @@
* limitations under the License.
*/
-package androidx.privacysandbox.tools.apicompiler.generator
+package androidx.privacysandbox.tools.core.generator.poet
-import com.squareup.kotlinpoet.FileSpec
-import java.io.OutputStream
-
-/** Convenience method to write [FileSpec]s to KSP-generated [OutputStream]s. */
-internal fun OutputStream.write(spec: FileSpec) = bufferedWriter().use(spec::writeTo)
+internal data class AidlPropertySpec(
+ val name: String,
+ val type: AidlTypeSpec,
+ val isNullable: Boolean = false,
+) {
+ override fun toString() = "${if (isNullable) "@nullable(heap=true) " else ""}$type $name;"
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
index 99433e0..aeee60f 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/poet/AidlTypeSpec.kt
@@ -18,6 +18,10 @@
import androidx.privacysandbox.tools.core.model.Type
-internal data class AidlTypeSpec(val innerType: Type, val requiresImport: Boolean = true) {
- override fun toString() = innerType.simpleName
+internal data class AidlTypeSpec(
+ val innerType: Type,
+ val requiresImport: Boolean = true,
+ val isList: Boolean = false
+) {
+ override fun toString() = innerType.simpleName + if (isList) "[]" else ""
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
index ea04578..467d8ac 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/model/Models.kt
@@ -23,6 +23,13 @@
return services.first()
}
+fun ParsedApi.hasSuspendFunctions(): Boolean {
+ val annotatedInterfaces = services + interfaces
+ return annotatedInterfaces
+ .flatMap(AnnotatedInterface::methods)
+ .any(Method::isSuspend)
+}
+
object Types {
val unit = Type(packageName = "kotlin", simpleName = "Unit")
val boolean = Type(packageName = "kotlin", simpleName = "Boolean")
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
index d09fd40..0f6e3f8 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/validator/ModelValidator.kt
@@ -96,14 +96,18 @@
private fun validateValuePropertyTypes() {
val allowedValuePropertyTypes =
- (api.values.map(AnnotatedValue::type) + Types.primitiveTypes).toSet()
+ (api.values.map(AnnotatedValue::type) +
+ api.interfaces.map(AnnotatedInterface::type) +
+ Types.primitiveTypes).toSet()
+
for (value in api.values) {
for (property in value.properties) {
if (!allowedValuePropertyTypes.contains(property.type)) {
errors.add(
"Error in ${value.type.qualifiedName}.${property.name}: " +
- "only primitives and data classes annotated with " +
- "@PrivacySandboxValue are supported as properties."
+ "only primitives, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as properties."
)
}
}
@@ -113,6 +117,7 @@
private fun validateCallbackMethods() {
val allowedParameterTypes =
(api.values.map(AnnotatedValue::type) +
+ api.interfaces.map(AnnotatedInterface::type) +
Types.primitiveTypes).toSet()
for (callback in api.callbacks) {
@@ -120,8 +125,9 @@
if (method.parameters.any { !allowedParameterTypes.contains(it.type) }) {
errors.add(
"Error in ${callback.type.qualifiedName}.${method.name}: " +
- "only primitives and data classes annotated with " +
- "@PrivacySandboxValue are supported as callback parameter types."
+ "only primitives, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as callback parameter types."
)
}
if (method.returnType != Types.unit || method.isSuspend) {
diff --git a/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto b/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto
new file mode 100644
index 0000000..788e3a7
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/main/proto/androidx/privacysandbox/tools/core/privacy_sandbox_tools_protocol.proto
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto3";
+
+package androidx.privacysandbox.tools.core;
+
+option java_package = "androidx.privacysandbox.tools.core.proto";
+option java_outer_classname = "PrivacySandboxToolsProtocol";
+
+// Metadata about Privacy Sandbox Tools.
+//
+// This message is stored in every SDK API descriptor so consumers can tweak code generation
+// according to what's expected by the compiled SDK.
+message ToolMetadata {
+ int32 code_generation_version = 1;
+}
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
index 69e30b3..60fdadb 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlCallbackGeneratorTest.kt
@@ -73,7 +73,6 @@
.containsExactly(
"com.mysdk" to "IMySdk",
"com.mysdk" to "IMyCallback",
- "com.mysdk" to "ICancellationSignal",
)
val outputTestDataDir = File("src/test/test-data/aidlcallbackgeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
index 3adb6d9..780e27c 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlInterfaceGeneratorTest.kt
@@ -85,6 +85,8 @@
"com.mysdk" to "IMyInterface",
"com.mysdk" to "ICancellationSignal",
"com.mysdk" to "IMyInterfaceTransactionCallback",
+ "com.mysdk" to "PrivacySandboxThrowableParcel",
+ "com.mysdk" to "ParcelableStackFrame",
)
val outputTestDataDir = File("src/test/test-data/aidlinterfacegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
index 1921b17..aeb2f9a 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlServiceGeneratorTest.kt
@@ -76,6 +76,8 @@
"com.mysdk" to "IStringTransactionCallback",
"com.mysdk" to "IUnitTransactionCallback",
"com.mysdk" to "ICancellationSignal",
+ "com.mysdk" to "PrivacySandboxThrowableParcel",
+ "com.mysdk" to "ParcelableStackFrame",
)
val outputTestDataDir = File("src/test/test-data/aidlservicegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
index bca3e3b..2b5ee15 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/AidlValueGeneratorTest.kt
@@ -96,6 +96,8 @@
"com.mysdk" to "IUnitTransactionCallback",
"com.mysdk" to "IOuterValueTransactionCallback",
"com.mysdk" to "ICancellationSignal",
+ "com.mysdk" to "PrivacySandboxThrowableParcel",
+ "com.mysdk" to "ParcelableStackFrame",
)
val outputTestDataDir = File("src/test/test-data/aidlvaluegeneratortest/output")
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
similarity index 74%
rename from privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
rename to privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
index 5c0a149..dc0ee76 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ClientBinderCodeConverterTest.kt
@@ -27,8 +27,8 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-class BinderCodeConverterTest {
- private val converter = BinderCodeConverter(
+class ClientBinderCodeConverterTest {
+ private val converter = ClientBinderCodeConverter(
ParsedApi(
services = setOf(
AnnotatedInterface(
@@ -75,31 +75,4 @@
).toString()
).isEqualTo("com.mysdk.CallbackClientProxy(callback)")
}
-
- @Test
- fun convertToBinderCode_primitive() {
- assertThat(
- converter.convertToBinderCode(
- Types.int, expression = "5"
- ).toString()
- ).isEqualTo("5")
- }
-
- @Test
- fun convertToBinderCode_value() {
- assertThat(
- converter.convertToBinderCode(
- Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
- ).toString()
- ).isEqualTo("com.mysdk.ValueConverter.toParcelable(value)")
- }
-
- @Test
- fun convertToBinderCode_callback() {
- assertThat(
- converter.convertToBinderCode(
- Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
- ).toString()
- ).isEqualTo("com.mysdk.CallbackStubDelegate(callback)")
- }
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
similarity index 74%
copy from privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
copy to privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
index 5c0a149..aeaaa53 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/BinderCodeConverterTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/generator/ServerBinderCodeConverterTest.kt
@@ -27,8 +27,8 @@
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
-class BinderCodeConverterTest {
- private val converter = BinderCodeConverter(
+class ServerBinderCodeConverterTest {
+ private val converter = ServerBinderCodeConverter(
ParsedApi(
services = setOf(
AnnotatedInterface(
@@ -50,33 +50,6 @@
)
@Test
- fun convertToModelCode_primitive() {
- assertThat(
- converter.convertToModelCode(
- Types.int, expression = "5"
- ).toString()
- ).isEqualTo("5")
- }
-
- @Test
- fun convertToModelCode_value() {
- assertThat(
- converter.convertToModelCode(
- Type(packageName = "com.mysdk", simpleName = "Value"), expression = "value"
- ).toString()
- ).isEqualTo("com.mysdk.ValueConverter.fromParcelable(value)")
- }
-
- @Test
- fun convertToModelCode_callback() {
- assertThat(
- converter.convertToModelCode(
- Type(packageName = "com.mysdk", simpleName = "Callback"), expression = "callback"
- ).toString()
- ).isEqualTo("com.mysdk.CallbackClientProxy(callback)")
- }
-
- @Test
fun convertToBinderCode_primitive() {
assertThat(
converter.convertToBinderCode(
diff --git a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
index 25525bc..c3a654d 100644
--- a/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
+++ b/privacysandbox/tools/tools-core/src/test/java/androidx/privacysandbox/tools/core/validator/ModelValidatorTest.kt
@@ -266,8 +266,9 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.Foo.bar: only primitives and data classes annotated with " +
- "@PrivacySandboxValue are supported as properties."
+ "Error in com.mysdk.Foo.bar: only primitives, data classes annotated with " +
+ "@PrivacySandboxValue and interfaces annotated with @PrivacySandboxInterface " +
+ "are supported as properties."
)
}
@@ -332,43 +333,9 @@
val validationResult = ModelValidator.validate(api)
assertThat(validationResult.isFailure).isTrue()
assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdkCallback.foo: only primitives and data classes annotated " +
- "with @PrivacySandboxValue are supported as callback parameter types."
- )
- }
-
- @Test
- fun callbackReceivingInterface_throws() {
- val api = ParsedApi(
- services = setOf(
- AnnotatedInterface(type = Type(packageName = "com.mysdk", simpleName = "MySdk")),
- ),
- callbacks = setOf(
- AnnotatedInterface(
- type = Type(packageName = "com.mysdk", simpleName = "MySdkCallback"),
- methods = listOf(
- Method(
- name = "foo",
- parameters = listOf(
- Parameter("otherCallback", Type("com.mysdk", "MySdkInterface"))
- ),
- returnType = Types.unit,
- isSuspend = false,
- ),
- )
- )
- ),
- interfaces = setOf(
- AnnotatedInterface(
- type = Type(packageName = "com.mysdk", simpleName = "MySdkInterface"),
- )
- )
- )
- val validationResult = ModelValidator.validate(api)
- assertThat(validationResult.isFailure).isTrue()
- assertThat(validationResult.errors).containsExactly(
- "Error in com.mysdk.MySdkCallback.foo: only primitives and data classes annotated " +
- "with @PrivacySandboxValue are supported as callback parameter types."
+ "Error in com.mysdk.MySdkCallback.foo: only primitives, data classes annotated " +
+ "with @PrivacySandboxValue and interfaces annotated with " +
+ "@PrivacySandboxInterface are supported as callback parameter types."
)
}
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl
deleted file mode 100644
index e336980..0000000
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlcallbackgeneratortest/output/com/mysdk/ICancellationSignal.aidl
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.mysdk;
-
-oneway interface ICancellationSignal {
- void cancel();
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
index a2d36bb..98e2b50 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterface.aidl
@@ -1,6 +1,5 @@
package com.mysdk;
-import com.mysdk.IMyInterface;
import com.mysdk.IMyInterfaceTransactionCallback;
oneway interface IMyInterface {
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
index a62ec9e..e920d2e 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/IMyInterfaceTransactionCallback.aidl
@@ -2,9 +2,10 @@
import com.mysdk.ICancellationSignal;
import com.mysdk.IMyInterface;
+import com.mysdk.PrivacySandboxThrowableParcel;
oneway interface IMyInterfaceTransactionCallback {
void onCancellable(ICancellationSignal cancellationSignal);
- void onFailure(int errorCode, String errorMessage);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
void onSuccess(IMyInterface result);
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+ String declaringClass;
+ String fileName;
+ String methodName;
+ int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlinterfacegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+ @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+ ParcelableStackFrame[] stackTrace;
+ PrivacySandboxThrowableParcel[] suppressedExceptions;
+ String errorMessage;
+ String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
index 11bba23..1cb6671 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IStringTransactionCallback.aidl
@@ -1,9 +1,10 @@
package com.mysdk;
import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
oneway interface IStringTransactionCallback {
void onCancellable(ICancellationSignal cancellationSignal);
- void onFailure(int errorCode, String errorMessage);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
void onSuccess(String result);
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
index a777df5..3e1856f 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
@@ -1,9 +1,10 @@
package com.mysdk;
import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
oneway interface IUnitTransactionCallback {
void onCancellable(ICancellationSignal cancellationSignal);
- void onFailure(int errorCode, String errorMessage);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
void onSuccess();
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+ String declaringClass;
+ String fileName;
+ String methodName;
+ int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlservicegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+ @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+ ParcelableStackFrame[] stackTrace;
+ PrivacySandboxThrowableParcel[] suppressedExceptions;
+ String errorMessage;
+ String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
index 89a0b1e..929fee2 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IOuterValueTransactionCallback.aidl
@@ -2,9 +2,10 @@
import com.mysdk.ICancellationSignal;
import com.mysdk.ParcelableOuterValue;
+import com.mysdk.PrivacySandboxThrowableParcel;
oneway interface IOuterValueTransactionCallback {
void onCancellable(ICancellationSignal cancellationSignal);
- void onFailure(int errorCode, String errorMessage);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
void onSuccess(in ParcelableOuterValue result);
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
index a777df5..3e1856f 100644
--- a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/IUnitTransactionCallback.aidl
@@ -1,9 +1,10 @@
package com.mysdk;
import com.mysdk.ICancellationSignal;
+import com.mysdk.PrivacySandboxThrowableParcel;
oneway interface IUnitTransactionCallback {
void onCancellable(ICancellationSignal cancellationSignal);
- void onFailure(int errorCode, String errorMessage);
+ void onFailure(in PrivacySandboxThrowableParcel throwableParcel);
void onSuccess();
}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
new file mode 100644
index 0000000..2050d2e
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/ParcelableStackFrame.aidl
@@ -0,0 +1,8 @@
+package com.mysdk;
+
+parcelable ParcelableStackFrame {
+ String declaringClass;
+ String fileName;
+ String methodName;
+ int lineNumber;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
new file mode 100644
index 0000000..716a0f9
--- /dev/null
+++ b/privacysandbox/tools/tools-core/src/test/test-data/aidlvaluegeneratortest/output/com/mysdk/PrivacySandboxThrowableParcel.aidl
@@ -0,0 +1,11 @@
+package com.mysdk;
+
+import com.mysdk.ParcelableStackFrame;
+
+parcelable PrivacySandboxThrowableParcel {
+ @nullable(heap=true) PrivacySandboxThrowableParcel cause;
+ ParcelableStackFrame[] stackTrace;
+ PrivacySandboxThrowableParcel[] suppressedExceptions;
+ String errorMessage;
+ String exceptionClass;
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 11df687..dec603f 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -17,10 +17,10 @@
package androidx.privacysandbox.tools.testing
import androidx.room.compiler.processing.util.DiagnosticLocation
-import androidx.room.compiler.processing.util.compiler.TestCompilationResult
import androidx.room.compiler.processing.util.DiagnosticMessage
import androidx.room.compiler.processing.util.Source
import androidx.room.compiler.processing.util.compiler.TestCompilationArguments
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
import androidx.room.compiler.processing.util.compiler.compile
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
@@ -61,12 +61,15 @@
fun assertThat(result: TestCompilationResult) = CompilationResultSubject(result)
}
+val TestCompilationResult.resourceOutputDir: File
+ get() = outputClasspath.first().parentFile.resolve("ksp-compiler/resourceOutputDir")
+
class CompilationResultSubject(private val result: TestCompilationResult) {
fun succeeds() {
assertWithMessage(
- "UnexpectedErrors:\n${getFullErrorMessages()?.joinToString("\n")}"
+ "Unexpected errors:\n${getFullErrorMessages().joinToString("\n")}"
).that(
- result.success
+ result.success && getRawErrorMessages().isEmpty()
).isTrue()
}
@@ -99,14 +102,19 @@
assertThat(getShortErrorMessages()).containsExactly(*errors)
}
+ private fun getRawErrorMessages() =
+ (result.diagnostics[Diagnostic.Kind.ERROR] ?: emptyList()) +
+ (result.diagnostics[Diagnostic.Kind.WARNING] ?: emptyList()) +
+ (result.diagnostics[Diagnostic.Kind.MANDATORY_WARNING] ?: emptyList())
+
private fun getShortErrorMessages() =
result.diagnostics[Diagnostic.Kind.ERROR]?.map(DiagnosticMessage::msg)
private fun getFullErrorMessages() =
- result.diagnostics[Diagnostic.Kind.ERROR]?.map { it.toFormattedMessage() }
+ getRawErrorMessages().map { it.toFormattedMessage() }
private fun DiagnosticMessage.toFormattedMessage() = """
- |Error: $msg
+ |$kind: $msg
|${location?.toFormattedLocation()}$
""".trimMargin()
@@ -132,71 +140,69 @@
// PrivacySandbox platform APIs are not available in AndroidX prebuilts nor are they stable, so
// while that's the case we use fake stubs to run our compilation tests.
val syntheticPrivacySandboxSources = listOf(
- Source.java(
- "android.app.sdksandbox.SdkSandboxManager", """
- |package android.app.sdksandbox;
+ Source.kotlin(
+ "androidx/privacysandbox/sdkruntime/core/SandboxedSdkCompat.kt", """
+ |package androidx.privacysandbox.sdkruntime.core
|
- |import android.os.Bundle;
- |import android.os.OutcomeReceiver;
- |import java.util.concurrent.Executor;
+ |import android.os.IBinder
|
- |public final class SdkSandboxManager {
- | public void loadSdk(
- | String sdkName,
- | Bundle params,
- | Executor executor,
- | OutcomeReceiver<SandboxedSdk, LoadSdkException> receiver) {}
- |}
- |""".trimMargin()
- ),
- Source.java(
- "android.app.sdksandbox.SandboxedSdk", """
- |package android.app.sdksandbox;
+ |@Suppress("UNUSED_PARAMETER")
+ |sealed class SandboxedSdkCompat {
+ | abstract fun getInterface(): IBinder?
|
- |import android.os.IBinder;
- |
- |public final class SandboxedSdk {
- | public SandboxedSdk(IBinder sdkInterface) {}
- | public IBinder getInterface() { return null; }
- |}
- |""".trimMargin()
- ),
- Source.java(
- "android.app.sdksandbox.SandboxedSdkProvider", """
- |package android.app.sdksandbox;
- |
- |import android.content.Context;
- |import android.os.Bundle;
- |import android.view.View;
- |
- |public abstract class SandboxedSdkProvider {
- | public abstract SandboxedSdk onLoadSdk(Bundle params) throws LoadSdkException;
- | public abstract View getView(
- | Context windowContext, Bundle params, int width, int height);
- | public final Context getContext() {
- | return null;
- | }
- | public abstract void onDataReceived(
- | Bundle data, DataReceivedCallback callback);
- | public interface DataReceivedCallback {
- | void onDataReceivedSuccess(Bundle params);
- | void onDataReceivedError(String errorMessage);
+ | companion object {
+ | fun create(binder: IBinder): SandboxedSdkCompat = throw RuntimeException("Stub!")
| }
|}
|""".trimMargin()
),
- Source.java(
- "android.app.sdksandbox.LoadSdkException", """
- |package android.app.sdksandbox;
+ Source.kotlin(
+ "androidx/privacysandbox/sdkruntime/client/SdkSandboxManagerCompat.kt", """
+ |package androidx.privacysandbox.sdkruntime.client
|
- |public final class LoadSdkException extends Exception {}
+ |import android.content.Context
+ |import android.os.Bundle
+ |import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
+ |
+ |@Suppress("UNUSED_PARAMETER")
+ |class SdkSandboxManagerCompat private constructor() {
+ | suspend fun loadSdk(
+ | sdkName: String,
+ | params: Bundle,
+ | ): SandboxedSdkCompat = throw RuntimeException("Stub!")
+ |
+ | companion object {
+ | fun obtain(context: Context): SdkSandboxManagerCompat =
+ | throw RuntimeException("Stub!")
+ | }
+ |}
|""".trimMargin()
),
- Source.java(
- "android.app.sdksandbox.SandboxedSdkContext", """
- |package android.app.sdksandbox;
+ Source.kotlin(
+ "androidx/privacysandbox/sdkruntime/core/SandboxedSdkProviderCompat.kt", """
+ |package androidx.privacysandbox.sdkruntime.core
|
- |public final class SandboxedSdkContext {}
+ |import android.content.Context
+ |import android.os.Bundle
+ |import android.view.View
+ |
+ |@Suppress("UNUSED_PARAMETER")
+ |abstract class SandboxedSdkProviderCompat {
+ | var context: Context? = null
+ | private set
+ | fun attachContext(context: Context): Unit = throw RuntimeException("Stub!")
+ |
+ | abstract fun onLoadSdk(params: Bundle): SandboxedSdkCompat
+ |
+ | open fun beforeUnloadSdk() {}
+ |
+ | abstract fun getView(
+ | windowContext: Context,
+ | params: Bundle,
+ | width: Int,
+ | height: Int
+ | ): View
+ |}
|""".trimMargin()
- ),
+ )
)
diff --git a/privacysandbox/ui/OWNERS b/privacysandbox/ui/OWNERS
new file mode 100644
index 0000000..329fc11
--- /dev/null
+++ b/privacysandbox/ui/OWNERS
@@ -0,0 +1,2 @@
+nator@google.com
+akulakov@google.com
diff --git a/privacysandbox/ui/ui-client/api/current.txt b/privacysandbox/ui/ui-client/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/api/res-current.txt b/privacysandbox/ui/ui-client/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/res-current.txt
diff --git a/privacysandbox/ui/ui-client/api/restricted_current.txt b/privacysandbox/ui/ui-client/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-client/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-client/build.gradle b/privacysandbox/ui/ui-client/build.gradle
new file mode 100644
index 0000000..8c3be5e
--- /dev/null
+++ b/privacysandbox/ui/ui-client/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.privacysandbox.ui.client"
+}
+
+androidx {
+ name = "androidx.privacysandbox.ui:ui-client"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+ inceptionYear = "2022"
+ description = "show UI from an SDKRuntime aware SDK"
+}
diff --git a/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
new file mode 100644
index 0000000..a414c19
--- /dev/null
+++ b/privacysandbox/ui/ui-client/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-client-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+Privacy Sandbox Ui Client
+
+# Package androidx.privacysandbox.ui.client
+
+This package lets an application host UI created by an SdkRuntime aware SDK.
diff --git a/privacysandbox/ui/ui-core/api/current.txt b/privacysandbox/ui/ui-core/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/api/res-current.txt b/privacysandbox/ui/ui-core/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/res-current.txt
diff --git a/privacysandbox/ui/ui-core/api/restricted_current.txt b/privacysandbox/ui/ui-core/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-core/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-core/build.gradle b/privacysandbox/ui/ui-core/build.gradle
new file mode 100644
index 0000000..6d1734d
--- /dev/null
+++ b/privacysandbox/ui/ui-core/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.privacysandbox.ui.core"
+}
+
+androidx {
+ name = "androidx.privacysandbox.ui:ui-core"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+ inceptionYear = "2022"
+ description = "contains core definitions for the privacysandbox ui library."
+}
diff --git a/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
new file mode 100644
index 0000000..aa56a66
--- /dev/null
+++ b/privacysandbox/ui/ui-core/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-core-documentation.md
@@ -0,0 +1,7 @@
+# Module root
+
+Privacy Sandbox Ui Core
+
+# Package androidx.privacysandbox.ui.core
+This package contains interface and class definitions shared between Privacy
+Sandbox Ui Client and Privacy Sandbox Ui Provider.
diff --git a/privacysandbox/ui/ui-provider/api/current.txt b/privacysandbox/ui/ui-provider/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/api/res-current.txt b/privacysandbox/ui/ui-provider/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/res-current.txt
diff --git a/privacysandbox/ui/ui-provider/api/restricted_current.txt b/privacysandbox/ui/ui-provider/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/privacysandbox/ui/ui-provider/build.gradle b/privacysandbox/ui/ui-provider/build.gradle
new file mode 100644
index 0000000..51a6f39
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/build.gradle
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ // Add dependencies here
+}
+
+android {
+ namespace "androidx.privacysandbox.ui.provider"
+}
+
+androidx {
+ name = "androidx.privacysandbox.ui:ui-provider"
+ type = LibraryType.PUBLISHED_LIBRARY
+ mavenGroup = LibraryGroups.PRIVACYSANDBOX_UI
+ inceptionYear = "2022"
+ description = "lets an SdkRuntime aware SDK share UI with a client application."
+}
diff --git a/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
new file mode 100644
index 0000000..cafa9ea
--- /dev/null
+++ b/privacysandbox/ui/ui-provider/src/main/java/androidx/privacysandbox/ui/androidx-privacysandbox-ui-ui-provider-documentation.md
@@ -0,0 +1,8 @@
+# Module root
+
+Privacy Sandbox Ui Provider
+
+# Package androidx.privacysandbox.ui.Provider
+
+This package lets an SdkRuntime aware SDKs share UI to be hosted in their
+client application.
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -4,6 +4,7 @@
public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
+ field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+ field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+ field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -4,6 +4,7 @@
public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
+ field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+ field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+ field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index 8eed65c..83b7dce 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -4,6 +4,7 @@
public class ProfileInstallReceiver extends android.content.BroadcastReceiver {
ctor public ProfileInstallReceiver();
method public void onReceive(android.content.Context, android.content.Intent?);
+ field public static final String ACTION_BENCHMARK_OPERATION = "androidx.profileinstaller.action.BENCHMARK_OPERATION";
field public static final String ACTION_INSTALL_PROFILE = "androidx.profileinstaller.action.INSTALL_PROFILE";
field public static final String ACTION_SAVE_PROFILE = "androidx.profileinstaller.action.SAVE_PROFILE";
field public static final String ACTION_SKIP_FILE = "androidx.profileinstaller.action.SKIP_FILE";
@@ -18,6 +19,9 @@
field public static final int DIAGNOSTIC_REF_PROFILE_EXISTS = 3; // 0x3
field public static final int RESULT_ALREADY_INSTALLED = 2; // 0x2
field public static final int RESULT_BASELINE_PROFILE_NOT_FOUND = 6; // 0x6
+ field public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15; // 0xf
+ field public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14; // 0xe
+ field public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16; // 0x10
field public static final int RESULT_DELETE_SKIP_FILE_SUCCESS = 11; // 0xb
field public static final int RESULT_DESIRED_FORMAT_UNSUPPORTED = 5; // 0x5
field public static final int RESULT_INSTALL_SKIP_FILE_SUCCESS = 10; // 0xa
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java
new file mode 100644
index 0000000..c57f7b4
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/BenchmarkOperation.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.profileinstaller;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.io.File;
+
+class BenchmarkOperation {
+ private BenchmarkOperation() {}
+
+ static void dropShaderCache(
+ @NonNull Context context,
+ @NonNull ProfileInstallReceiver.ResultDiagnostics callback
+ ) {
+ File shaderDirectory;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ // shaders started using code cache dir once it was added in N
+ shaderDirectory = Api24ContextHelper.getDeviceProtectedCodeCacheDir(context);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ // getCodeCacheDir was added in L, but not used by platform for shaders until M
+ shaderDirectory = Api21ContextHelper.getCodeCacheDir(context);
+ } else {
+ shaderDirectory = context.getCacheDir();
+ }
+ if (deleteFilesRecursively(shaderDirectory)) {
+ callback.onResultReceived(ProfileInstaller.RESULT_BENCHMARK_OPERATION_SUCCESS, null);
+ } else {
+ callback.onResultReceived(ProfileInstaller.RESULT_BENCHMARK_OPERATION_FAILURE, null);
+ }
+
+ }
+
+ /**
+ * Returns true on success
+ *
+ * If hits a failure to access a directory, returns false but keeps going
+ */
+ static boolean deleteFilesRecursively(File file) {
+ if (file.isDirectory()) {
+ File[] children = file.listFiles();
+ if (children == null) {
+ return false;
+ }
+ boolean success = true;
+ for (File child : children) {
+ success = deleteFilesRecursively(child) && success;
+ }
+ return success;
+ } else {
+ //noinspection ResultOfMethodCallIgnored
+ file.delete();
+ return true;
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ private static class Api21ContextHelper {
+ static File getCodeCacheDir(Context context) {
+ // Code cache dir added in 21
+ return context.getCodeCacheDir();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private static class Api24ContextHelper {
+ static File getDeviceProtectedCodeCacheDir(Context context) {
+ // Code device protected storage added in 24
+ return context.createDeviceProtectedStorageContext().getCodeCacheDir();
+ }
+ }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
index 31f7354..294c6ed 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstallReceiver.java
@@ -53,6 +53,7 @@
*/
public static final @NonNull String ACTION_SAVE_PROFILE =
"androidx.profileinstaller.action.SAVE_PROFILE";
+
/**
* This is an action constant which requests that {@link ProfileInstaller} manipulate the
* skip file used during profile installation. This is only useful when the app is being
@@ -62,6 +63,13 @@
"androidx.profileinstaller.action.SKIP_FILE";
/**
+ * This is an action that triggers actions required for stable benchmarking from an external
+ * tool on user builds, such as clearing the code cache, or triggering garbage collection.
+ */
+ public static final @NonNull String ACTION_BENCHMARK_OPERATION =
+ "androidx.profileinstaller.action.BENCHMARK_OPERATION";
+
+ /**
* This is the key in the {@link Bundle} of extras, which provides additional information on
* the operation to be performed.
*/
@@ -76,6 +84,18 @@
*/
private static final @NonNull String EXTRA_SKIP_FILE_OPERATION_DELETE = "DELETE_SKIP_FILE";
+ /**
+ * This is the key in the {@link Bundle} of extras, which provides additional information on
+ * the operation to be performed.
+ */
+ private static final @NonNull String EXTRA_BENCHMARK_OPERATION = "EXTRA_BENCHMARK_OPERATION";
+
+ /**
+ * The value that requests the shader cache be dropped.
+ */
+ private static final @NonNull String EXTRA_BENCHMARK_OPERATION_DROP_SHADER_CACHE =
+ "DROP_SHADER_CACHE";
+
@Override
public void onReceive(@NonNull Context context, @Nullable Intent intent) {
if (intent == null) return;
@@ -96,6 +116,20 @@
}
} else if (ACTION_SAVE_PROFILE.equals(action)) {
saveProfile(new ResultDiagnostics());
+ } else if (ACTION_BENCHMARK_OPERATION.equals(action)) {
+ Bundle extras = intent.getExtras();
+ if (extras != null) {
+ String operation = extras.getString(EXTRA_BENCHMARK_OPERATION);
+ ResultDiagnostics diagnostics = new ResultDiagnostics();
+ if (EXTRA_BENCHMARK_OPERATION_DROP_SHADER_CACHE.equals(operation)) {
+ BenchmarkOperation.dropShaderCache(context, diagnostics);
+ } else {
+ diagnostics.onResultReceived(
+ ProfileInstaller.RESULT_BENCHMARK_OPERATION_UNKNOWN,
+ null
+ );
+ }
+ }
}
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 357b96f..f9c63c5 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -237,7 +237,10 @@
RESULT_INSTALL_SKIP_FILE_SUCCESS,
RESULT_DELETE_SKIP_FILE_SUCCESS,
RESULT_SAVE_PROFILE_SIGNALLED,
- RESULT_SAVE_PROFILE_SKIPPED
+ RESULT_SAVE_PROFILE_SKIPPED,
+ RESULT_BENCHMARK_OPERATION_SUCCESS,
+ RESULT_BENCHMARK_OPERATION_FAILURE,
+ RESULT_BENCHMARK_OPERATION_UNKNOWN
})
public @interface ResultCode {}
@@ -321,6 +324,22 @@
@ResultCode public static final int RESULT_SAVE_PROFILE_SKIPPED = 13;
/**
+ * Indicates that the benchmark operation was successful
+ */
+ @ResultCode public static final int RESULT_BENCHMARK_OPERATION_SUCCESS = 14;
+
+ /**
+ * Indicates that the benchmark operation failed
+ */
+ @ResultCode public static final int RESULT_BENCHMARK_OPERATION_FAILURE = 15;
+
+ /**
+ * Indicates that the benchmark operation was unknown, likely meaning profileinstaller needs
+ * to update to support the operation
+ */
+ @ResultCode public static final int RESULT_BENCHMARK_OPERATION_UNKNOWN = 16;
+
+ /**
* Check if we've already installed a profile for this app installation.
*
* @hide
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
index 7c8f178..baadf37 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerSnappingTest.java
@@ -27,6 +27,7 @@
import androidx.annotation.Nullable;
import androidx.test.filters.LargeTest;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@@ -73,6 +74,7 @@
return rv;
}
+ @Ignore // b/244324972
@Test
public void snapOnScrollSameView() throws Throwable {
final Config config = (Config) mConfig.clone();
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index ffcc174..b56858e 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -34,11 +34,14 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
+import android.os.Bundle;
import android.util.SparseIntArray;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.GridView;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
@@ -858,6 +861,151 @@
assertEquals(GridView.class.getName(), info.getClassName());
}
+ @Test
+ public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
+ throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 0));
+ waitForFirstLayout(recyclerView);
+
+ final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
+ }
+ });
+
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
+ public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
+ throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 1));
+ waitForFirstLayout(recyclerView);
+
+ final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mGlm.onInitializeAccessibilityNodeInfo(nodeInfo);
+ }
+ });
+
+ assertTrue(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_withNullArgs_returnsFalse()
+ throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+ waitForFirstLayout(recyclerView);
+
+ final boolean[] returnValue = {false};
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mGlm.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, null);
+ }
+ });
+
+ assertFalse(returnValue[0]);
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_noRow_returnsFalse()
+ throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+ waitForFirstLayout(recyclerView);
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
+
+ final boolean[] returnValue = {false};
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mGlm.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, bundle);
+ }
+ });
+
+ assertFalse(returnValue[0]);
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_noColumn_returnsFalse()
+ throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+ waitForFirstLayout(recyclerView);
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+
+ final boolean[] returnValue = {false};
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mGlm.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, bundle);
+ }
+ });
+
+ assertFalse(returnValue[0]);
+ }
+
+ @Test
+ public void performAccessibilityAction_withValidRowAndColumn_performsScroll() throws Throwable {
+ final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+ final GridLayoutManager.SpanSizeLookup spanSizeLookup =
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ if (position % 5 == 0) {
+ return 2;
+ }
+ return 1;
+ }
+ };
+
+ mGlm.setOrientation(RecyclerView.HORIZONTAL);
+ mGlm.setSpanSizeLookup(spanSizeLookup);
+ /*
+ This generates the following grid, with items 1, 6, 11, etc. (at indices 0, 5, 10, etc.)
+ spanning two rows.
+ 1 3 6 8 11 13 16 etc.
+ 4 9 14 etc.
+ 2 5 7 10 12 15 17 etc.
+ */
+ waitForFirstLayout(recyclerView);
+ mGlm.expectLayout(1);
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 0);
+ bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 2);
+
+ final boolean[] returnValue = {false};
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = recyclerView.getLayoutManager().performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, bundle);
+ }
+ });
+ mGlm.waitForLayout(2);
+
+ assertTrue(returnValue[0]);
+ assertEquals(((TextView) mGlm.getChildAt(0)).getText(), "Item (6)");
+ }
+
public GridLayoutManager.LayoutParams ensureGridLp(View view) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
GridLayoutManager.LayoutParams glp;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index 4c4aba2..d18f0b9 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -17,6 +17,7 @@
import android.content.Context;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseIntArray;
@@ -25,6 +26,7 @@
import android.widget.GridView;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import java.util.Arrays;
@@ -40,6 +42,7 @@
private static final boolean DEBUG = false;
private static final String TAG = "GridLayoutManager";
public static final int DEFAULT_SPAN_COUNT = -1;
+
/**
* Span size have been changed but we've not done a new layout calculation.
*/
@@ -175,6 +178,57 @@
}
@Override
+ boolean performAccessibilityAction(int action, @Nullable Bundle args) {
+ if (action == android.R.id.accessibilityActionScrollToPosition) {
+ final int noRow = -1;
+ final int noColumn = -1;
+ if (args != null) {
+ int rowArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT,
+ noRow);
+ int columnArg = args.getInt(AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT,
+ noColumn);
+
+ if (rowArg == noRow || columnArg == noColumn) {
+ return false;
+ }
+
+ int itemCount = mRecyclerView.mAdapter.getItemCount();
+
+ int position = -1;
+ for (int i = 0; i < itemCount; i++) {
+ // Corresponds to a column value if the orientation is VERTICAL and a row value
+ // if the orientation is HORIZONTAL
+ int spanIndex = getSpanIndex(mRecyclerView.mRecycler, mRecyclerView.mState, i);
+
+ // Corresponds to a row value if the orientation is VERTICAL and a column value
+ // if the orientation is HORIZONTAL
+ int spanGroupIndex = getSpanGroupIndex(mRecyclerView.mRecycler,
+ mRecyclerView.mState, i);
+
+ if (mOrientation == VERTICAL) {
+ if (spanIndex == columnArg && spanGroupIndex == rowArg) {
+ position = i;
+ break;
+ }
+ } else { // horizontal
+ if (spanIndex == rowArg && spanGroupIndex == columnArg) {
+ position = i;
+ break;
+ }
+ }
+ }
+
+ if (position > -1) {
+ scrollToPositionWithOffset(position, 0);
+ return true;
+ }
+ return false;
+ }
+ }
+ return super.performAccessibilityAction(action, args);
+ }
+
+ @Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.isPreLayout()) {
cachePreLayoutSpanMapping();
@@ -1452,5 +1506,4 @@
return mSpanSize;
}
}
-
-}
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing-testing/build.gradle b/room/room-compiler-processing-testing/build.gradle
index e314215..a26f969 100644
--- a/room/room-compiler-processing-testing/build.gradle
+++ b/room/room-compiler-processing-testing/build.gradle
@@ -37,14 +37,12 @@
}
/**
- * Create a properties file with versions that can be read from the test helper to setup test
- * projects.
+ * Create a properties file with versions that can be read from the test helper to setup test projects.
* see: b/178725084
*/
-def testPropsOutDir = project.layout.buildDirectory.dir("test-config")
def writeTestPropsTask = tasks.register("prepareTestConfiguration", WriteProperties.class) {
description = "Generates a properties file with the current environment for compilation tests"
- setOutputFile(testPropsOutDir.map {
+ setOutputFile(project.layout.buildDirectory.dir("test-config").map {
it.file("androidx.room.compiler.processing.util.CompilationTestCapabilities.Config" +
".properties")
})
@@ -55,18 +53,11 @@
java {
sourceSets {
main {
- resources.srcDir(testPropsOutDir)
+ resources.srcDir(writeTestPropsTask.map { it.outputFile.parentFile })
}
}
}
-tasks.named("sourceJar").configure {
- dependsOn(writeTestPropsTask)
-}
-tasks.named("processResources").configure {
- dependsOn(writeTestPropsTask)
-}
-
// enable opt in only for tests so that we don't create non experimental APIs by mistake
// in the source.
tasks.named("compileTestKotlin", KotlinCompile.class).configure {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 18048d5..6f3ad36 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -32,8 +32,10 @@
import com.squareup.kotlinpoet.javapoet.JClassName
import com.squareup.kotlinpoet.javapoet.JParameterizedTypeName
import com.squareup.kotlinpoet.javapoet.JTypeName
+import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
import com.squareup.kotlinpoet.javapoet.KClassName
import com.squareup.kotlinpoet.javapoet.KTypeName
+import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
import kotlin.reflect.KClass
/**
@@ -105,6 +107,11 @@
val PRIMITIVE_FLOAT = Float::class.asPrimitiveTypeName()
val PRIMITIVE_DOUBLE = Double::class.asPrimitiveTypeName()
+ val ANY_WILDCARD = XTypeName(
+ java = JWildcardTypeName.subtypeOf(Object::class.java),
+ kotlin = com.squareup.kotlinpoet.STAR
+ )
+
/**
* The default [KTypeName] returned by xprocessing APIs when the backend is not KSP.
*/
@@ -126,7 +133,7 @@
* the equivalent Kotlin and Java type names are represented, [IntArray] and `int[]`
* respectively.
*/
- fun getArrayTypeName(componentTypeName: XTypeName): XTypeName {
+ fun getArrayName(componentTypeName: XTypeName): XTypeName {
val (java, kotlin) = when (componentTypeName) {
PRIMITIVE_BOOLEAN ->
JArrayTypeName.of(JTypeName.BOOLEAN) to BOOLEAN_ARRAY
@@ -150,6 +157,34 @@
}
return XTypeName(java, kotlin)
}
+
+ /**
+ * Create a contravariant wildcard type name, to use as a consumer site-variance
+ * declaration.
+ *
+ * In Java: `? super <bound>`
+ * In Kotlin `in <bound>
+ */
+ fun getConsumerSuperName(bound: XTypeName): XTypeName {
+ return XTypeName(
+ java = JWildcardTypeName.supertypeOf(bound.java),
+ kotlin = KWildcardTypeName.consumerOf(bound.kotlin)
+ )
+ }
+
+ /**
+ * Create a covariant wildcard type name, to use as a producer site-variance
+ * declaration.
+ *
+ * In Java: `? extends <bound>`
+ * In Kotlin `out <bound>
+ */
+ fun getProducerExtendsName(bound: XTypeName): XTypeName {
+ return XTypeName(
+ java = JWildcardTypeName.subtypeOf(bound.java),
+ kotlin = KWildcardTypeName.producerOf(bound.kotlin)
+ )
+ }
}
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
index a88bb07..bcdf483 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
@@ -65,7 +65,7 @@
object RoomTypeNames {
val STRING_UTIL: XClassName = XClassName.get("$ROOM_PACKAGE.util", "StringUtil")
val ROOM_DB: XClassName = XClassName.get(ROOM_PACKAGE, "RoomDatabase")
- val ROOM_DB_KT: ClassName = ClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
+ val ROOM_DB_KT = XClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
val ROOM_DB_CONFIG: ClassName = ClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
val INSERTION_ADAPTER: XClassName =
XClassName.get(ROOM_PACKAGE, "EntityInsertionAdapter")
@@ -247,7 +247,7 @@
object KotlinTypeNames {
val UNIT = ClassName.get("kotlin", "Unit")
- val CONTINUATION = ClassName.get("kotlin.coroutines", "Continuation")
+ val CONTINUATION = XClassName.get("kotlin.coroutines", "Continuation")
val COROUTINE_SCOPE = ClassName.get("kotlinx.coroutines", "CoroutineScope")
val CHANNEL = ClassName.get("kotlinx.coroutines.channels", "Channel")
val RECEIVE_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "ReceiveChannel")
@@ -260,6 +260,8 @@
RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
val ROOM_SQL_QUERY_ACQUIRE =
RoomTypeNames.ROOM_SQL_QUERY.companionMember("acquire", isJvmStatic = true)
+ val ROOM_DATABASE_WITH_TRANSACTION =
+ RoomTypeNames.ROOM_DB_KT.packageMember("withTransaction")
}
val DEFERRED_TYPES = listOf(
@@ -332,29 +334,29 @@
)
}
-fun Function1TypeSpecBuilder(
- parameterTypeName: TypeName,
+fun Function1TypeSpec(
+ language: CodeLanguage,
+ parameterTypeName: XTypeName,
parameterName: String,
- returnTypeName: TypeName,
- callBody: MethodSpec.Builder.() -> Unit
-) = TypeSpec.anonymousClassBuilder("").apply {
+ returnTypeName: XTypeName,
+ callBody: XFunSpec.Builder.() -> Unit
+) = XTypeSpec.anonymousClassBuilder(language, "").apply {
superclass(
- ParameterizedTypeName.get(
- Function1::class.typeName,
- parameterTypeName,
- returnTypeName
- )
+ Function1::class.asClassName().parametrizedBy(parameterTypeName, returnTypeName)
)
- addMethod(
- MethodSpec.methodBuilder("invoke").apply {
+ addFunction(
+ XFunSpec.builder(
+ language = language,
+ name = "invoke",
+ visibility = VisibilityModifier.PUBLIC,
+ isOverride = true
+ ).apply {
addParameter(parameterTypeName, parameterName)
returns(returnTypeName)
- addModifiers(Modifier.PUBLIC)
- addAnnotation(Override::class.java)
callBody()
}.build()
)
-}
+}.build()
/**
* Generates a 2D array literal where the value at `i`,`j` will be produced by `valueProducer.
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index b299f50..043a9896 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -190,7 +190,7 @@
override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
InstantTransactionMethodBinder(
- TransactionMethodAdapter(executableElement.jvmName, callType)
+ TransactionMethodAdapter(executableElement.name, executableElement.jvmName, callType)
)
}
@@ -206,7 +206,7 @@
private val continuationParam: XVariableElement by lazy {
val continuationType = context.processingEnv
- .requireType(KotlinTypeNames.CONTINUATION.toString()).rawType
+ .requireType(KotlinTypeNames.CONTINUATION).rawType
executableElement.parameters.last {
it.type.rawType == continuationType
}
@@ -300,8 +300,12 @@
override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
CoroutineTransactionMethodBinder(
- adapter = TransactionMethodAdapter(executableElement.jvmName, callType),
+ adapter = TransactionMethodAdapter(
+ executableElement.name,
+ executableElement.jvmName,
+ callType
+ ),
continuationParamName = continuationParam.name,
- useLambdaSyntax = context.processingEnv.jvmVersion >= 8
+ javaLambdaSyntaxAvailable = context.processingEnv.jvmVersion >= 8
)
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
index 4c8dff9..5df5ca4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableDeleteOrUpdateMethodBinder.kt
@@ -17,6 +17,8 @@
package androidx.room.solver.shortcut.binder
import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.toJavaPoet
import androidx.room.compiler.processing.XType
import androidx.room.ext.CallableTypeSpecBuilder
import androidx.room.solver.CodeGenScope
@@ -49,8 +51,8 @@
override fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
- adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
- dbField: FieldSpec,
+ adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
) {
val adapterScope = scope.fork()
@@ -58,14 +60,14 @@
adapter?.createDeleteOrUpdateMethodBody(
parameters = parameters,
adapters = adapters,
- dbField = dbField,
+ dbProperty = dbProperty,
scope = adapterScope
)
addCode(adapterScope.builder().build())
}.build()
scope.builder().apply {
- addStmntBlock(callableImpl, dbField)
+ addStmntBlock(callableImpl, dbProperty.toJavaPoet())
}
}
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
index 08a7d50..2fcce7c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/DeleteOrUpdateMethodBinder.kt
@@ -17,11 +17,10 @@
package androidx.room.solver.shortcut.binder
import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.solver.CodeGenScope
import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeSpec
/**
* Connects the delete or update method, the database and the [DeleteOrUpdateMethodAdapter].
@@ -56,8 +55,8 @@
*/
abstract fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
- adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
- dbField: FieldSpec,
+ adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
)
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
index c8de492..7c74c77 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantDeleteOrUpdateMethodBinder.kt
@@ -17,12 +17,10 @@
package androidx.room.solver.shortcut.binder
import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.solver.CodeGenScope
import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeSpec
/**
* Binder that knows how to write instant (blocking) delete and update methods.
@@ -33,17 +31,17 @@
override fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
- adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
- dbField: FieldSpec,
+ adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
) {
- scope.builder().apply {
- addStatement("$N.assertNotSuspendingTransaction()", dbField)
+ scope.builder.apply {
+ addStatement("%N.assertNotSuspendingTransaction()", dbProperty)
}
adapter?.createDeleteOrUpdateMethodBody(
parameters = parameters,
adapters = adapters,
- dbField = dbField,
+ dbProperty = dbProperty,
scope = scope
)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
index 7c64f3f..2fad367 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -17,8 +17,6 @@
package androidx.room.solver.shortcut.binder
import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.N
import androidx.room.solver.CodeGenScope
import androidx.room.solver.shortcut.result.InsertOrUpsertMethodAdapter
import androidx.room.vo.ShortcutQueryParameter
@@ -35,9 +33,8 @@
dbProperty: XPropertySpec,
scope: CodeGenScope
) {
- val dbField = dbProperty.toJavaPoet()
- scope.builder().apply {
- addStatement("$N.assertNotSuspendingTransaction()", dbField)
+ scope.builder.apply {
+ addStatement("%N.assertNotSuspendingTransaction()", dbProperty)
}
adapter?.createMethodBody(
parameters = parameters,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
index 0d164c9..e754baa 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/DeleteOrUpdateMethodAdapter.kt
@@ -16,21 +16,22 @@
package androidx.room.solver.shortcut.result
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.processing.XType
import androidx.room.compiler.processing.isInt
import androidx.room.compiler.processing.isKotlinUnit
import androidx.room.compiler.processing.isVoid
import androidx.room.compiler.processing.isVoidObject
import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.T
+import androidx.room.ext.isNotKotlinUnit
+import androidx.room.ext.isNotVoid
+import androidx.room.ext.isNotVoidObject
import androidx.room.solver.CodeGenScope
import androidx.room.vo.ShortcutQueryParameter
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
/**
* Class that knows how to generate a delete or update method body.
@@ -54,8 +55,8 @@
fun createDeleteOrUpdateMethodBody(
parameters: List<ShortcutQueryParameter>,
- adapters: Map<String, Pair<XPropertySpec, TypeSpec>>,
- dbField: FieldSpec,
+ adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
) {
val resultVar = if (hasResultValue(returnType)) {
@@ -63,45 +64,46 @@
} else {
null
}
- scope.builder().apply {
+ scope.builder.apply {
if (resultVar != null) {
- addStatement("$T $L = 0", TypeName.INT, resultVar)
+ addLocalVariable(
+ name = resultVar,
+ typeName = XTypeName.PRIMITIVE_INT,
+ isMutable = true,
+ assignExpr = XCodeBlock.of(language, "0")
+ )
}
- addStatement("$N.beginTransaction()", dbField)
+ addStatement("%N.beginTransaction()", dbProperty)
beginControlFlow("try").apply {
parameters.forEach { param ->
val adapter = adapters.getValue(param.name).first
addStatement(
- "$L$L.$L($L)",
- if (resultVar == null) "" else "$resultVar +=",
- adapter.name, param.handleMethodName(), param.name
+ "%L%L.%L(%L)",
+ if (resultVar == null) "" else "$resultVar += ",
+ adapter.name,
+ param.handleMethodName(),
+ param.name
)
}
- addStatement("$N.setTransactionSuccessful()", dbField)
+ addStatement("%N.setTransactionSuccessful()", dbProperty)
if (resultVar != null) {
- addStatement("return $L", resultVar)
- } else if (hasNullReturn(returnType)) {
+ addStatement("return %L", resultVar)
+ } else if (returnType.isVoidObject()) {
addStatement("return null")
- } else if (hasUnitReturn(returnType)) {
- addStatement("return $T.INSTANCE", KotlinTypeNames.UNIT)
+ } else if (returnType.isKotlinUnit() && scope.language == CodeLanguage.JAVA) {
+ addStatement("return %T.INSTANCE", KotlinTypeNames.UNIT)
}
}
nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
+ addStatement("%N.endTransaction()", dbProperty)
}
endControlFlow()
}
}
private fun hasResultValue(returnType: XType): Boolean {
- return !(
- returnType.isVoid() ||
- returnType.isVoidObject() ||
- returnType.isKotlinUnit()
- )
+ return returnType.isNotVoid() &&
+ returnType.isNotVoidObject() &&
+ returnType.isNotKotlinUnit()
}
-
- private fun hasNullReturn(returnType: XType) = returnType.isVoidObject()
-
- private fun hasUnitReturn(returnType: XType) = returnType.isKotlinUnit()
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
index 309859e..56d437e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/result/InsertOrUpsertMethodAdapter.kt
@@ -132,7 +132,6 @@
)
}
- @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private fun getReturnType(returnType: XType): ReturnType? {
return if (returnType.isVoid()) {
ReturnType.VOID
@@ -250,11 +249,11 @@
SINGLE_ID("AndReturnId", XTypeName.PRIMITIVE_LONG), // return long
ID_ARRAY(
"AndReturnIdsArray",
- XTypeName.getArrayTypeName(XTypeName.PRIMITIVE_LONG)
+ XTypeName.getArrayName(XTypeName.PRIMITIVE_LONG)
), // return long[]
ID_ARRAY_BOX(
"AndReturnIdsArrayBox",
- XTypeName.getArrayTypeName(Long::class.asClassName())
+ XTypeName.getArrayName(Long::class.asClassName())
), // return Long[]
ID_LIST(
"AndReturnIdsList",
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
index 9d44ef0..caada40 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
@@ -16,20 +16,20 @@
package androidx.room.solver.transaction.binder
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.XType
-import androidx.room.ext.Function1TypeSpecBuilder
-import androidx.room.ext.KotlinTypeNames.CONTINUATION
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.ROOM_DB_KT
-import androidx.room.ext.T
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.KotlinTypeNames
+import androidx.room.ext.RoomMemberNames
+import androidx.room.ext.isNotKotlinUnit
import androidx.room.solver.CodeGenScope
import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.WildcardTypeName
/**
* Binder that knows how to write suspending transaction wrapper methods.
@@ -37,45 +37,96 @@
class CoroutineTransactionMethodBinder(
adapter: TransactionMethodAdapter,
private val continuationParamName: String,
- private val useLambdaSyntax: Boolean
+ private val javaLambdaSyntaxAvailable: Boolean
) : TransactionMethodBinder(adapter) {
override fun executeAndReturn(
returnType: XType,
parameterNames: List<String>,
- daoName: ClassName,
- daoImplName: ClassName,
- dbField: FieldSpec,
+ daoName: XClassName,
+ daoImplName: XClassName,
+ dbProperty: XPropertySpec,
+ scope: CodeGenScope
+ ) {
+ when (scope.language) {
+ CodeLanguage.JAVA -> executeAndReturnJava(
+ returnType, parameterNames, daoName, daoImplName, dbProperty, scope
+ )
+ CodeLanguage.KOTLIN -> executeAndReturnKotlin(
+ returnType, parameterNames, daoName, daoImplName, dbProperty, scope
+ )
+ }
+ }
+
+ private fun executeAndReturnJava(
+ returnType: XType,
+ parameterNames: List<String>,
+ daoName: XClassName,
+ daoImplName: XClassName,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
) {
val innerContinuationParamName = "__cont"
val adapterScope = scope.fork()
adapter.createDelegateToSuperCode(
- returnType = returnType,
parameterNames = parameterNames + innerContinuationParamName,
daoName = daoName,
daoImplName = daoImplName,
- returnStmt = !useLambdaSyntax,
+ returnStmt = !javaLambdaSyntaxAvailable,
scope = adapterScope
)
- val functionImpl: Any = if (useLambdaSyntax) {
- CodeBlock.of("($L) -> $L", innerContinuationParamName, adapterScope.builder().build())
+ val functionImpl: Any = if (javaLambdaSyntaxAvailable) {
+ XCodeBlock.of(
+ scope.language,
+ "(%L) -> %L",
+ innerContinuationParamName, adapterScope.builder().build()
+ )
} else {
- Function1TypeSpecBuilder(
- parameterTypeName = ParameterizedTypeName.get(
- CONTINUATION, WildcardTypeName.supertypeOf(returnType.typeName)
+ Function1TypeSpec(
+ language = scope.language,
+ parameterTypeName = KotlinTypeNames.CONTINUATION.parametrizedBy(
+ XTypeName.getConsumerSuperName(returnType.asTypeName())
),
parameterName = innerContinuationParamName,
- returnTypeName = ClassName.OBJECT
+ returnTypeName = Any::class.asClassName()
) {
- addStatement(adapterScope.builder().build())
- }.build()
+ addStatement("%L", adapterScope.generate())
+ }
}
- scope.builder().apply {
- addStatement(
- "return $T.withTransaction($N, $L, $N)",
- ROOM_DB_KT, dbField, functionImpl, continuationParamName
+ scope.builder.addStatement(
+ "return %M(%N, %L, %L)",
+ RoomMemberNames.ROOM_DATABASE_WITH_TRANSACTION,
+ dbProperty,
+ functionImpl,
+ continuationParamName
+ )
+ }
+
+ private fun executeAndReturnKotlin(
+ returnType: XType,
+ parameterNames: List<String>,
+ daoName: XClassName,
+ daoImplName: XClassName,
+ dbProperty: XPropertySpec,
+ scope: CodeGenScope
+ ) {
+ scope.builder.apply {
+ if (returnType.isNotKotlinUnit()) {
+ add("return ")
+ }
+ beginControlFlow(
+ "%N.%M",
+ dbProperty, RoomMemberNames.ROOM_DATABASE_WITH_TRANSACTION
)
+ val adapterScope = scope.fork()
+ adapter.createDelegateToSuperCode(
+ parameterNames = parameterNames,
+ daoName = daoName,
+ daoImplName = daoImplName,
+ scope = adapterScope
+ )
+ addStatement("%L", adapterScope.generate())
+ endControlFlow()
}
}
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
index 4e41b81..6510c5b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/InstantTransactionMethodBinder.kt
@@ -16,13 +16,16 @@
package androidx.room.solver.transaction.binder
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.asClassName
import androidx.room.compiler.processing.XType
-import androidx.room.ext.N
+import androidx.room.compiler.processing.isKotlinUnit
+import androidx.room.ext.isNotKotlinUnit
import androidx.room.ext.isNotVoid
import androidx.room.solver.CodeGenScope
import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
/**
* Binder that knows how to write instant (blocking) transaction wrapper methods.
@@ -33,39 +36,46 @@
override fun executeAndReturn(
returnType: XType,
parameterNames: List<String>,
- daoName: ClassName,
- daoImplName: ClassName,
- dbField: FieldSpec,
+ daoName: XClassName,
+ daoImplName: XClassName,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
) {
- scope.builder().apply {
- addStatement("$N.beginTransaction()", dbField)
+ scope.builder.apply {
+ addStatement("%N.beginTransaction()", dbProperty)
beginControlFlow("try").apply {
- val returnsValue = returnType.isNotVoid()
+ val returnsValue = returnType.isNotVoid() && returnType.isNotKotlinUnit()
val resultVar = if (returnsValue) {
scope.getTmpVar("_result")
} else {
null
}
+ if (resultVar != null) {
+ addLocalVariable(
+ name = resultVar,
+ typeName = returnType.asTypeName()
+ )
+ }
val adapterScope = scope.fork()
adapter.createDelegateToSuperCode(
- returnType = returnType,
parameterNames = parameterNames,
daoName = daoName,
daoImplName = daoImplName,
resultVar = resultVar,
scope = adapterScope
)
- addStatement(adapterScope.builder().build())
+ addStatement("%L", adapterScope.generate())
- addStatement("$N.setTransactionSuccessful()", dbField)
- if (returnsValue) {
- addStatement("return $N", resultVar)
+ addStatement("%N.setTransactionSuccessful()", dbProperty)
+ if (resultVar != null) {
+ addStatement("return %N", resultVar)
+ } else if (returnType.isKotlinUnit() && language == CodeLanguage.JAVA) {
+ addStatement("return %T.INSTANCE", Unit::class.asClassName())
}
}
nextControlFlow("finally").apply {
- addStatement("$N.endTransaction()", dbField)
+ addStatement("%N.endTransaction()", dbProperty)
}
endControlFlow()
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
index fb5f27b..7b32d8c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/TransactionMethodBinder.kt
@@ -16,11 +16,11 @@
package androidx.room.solver.transaction.binder
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XPropertySpec
import androidx.room.compiler.processing.XType
import androidx.room.solver.CodeGenScope
import androidx.room.solver.transaction.result.TransactionMethodAdapter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.FieldSpec
/**
* Connects a transaction method, database and a [TransactionMethodAdapter].
@@ -38,9 +38,9 @@
abstract fun executeAndReturn(
returnType: XType,
parameterNames: List<String>,
- daoName: ClassName,
- daoImplName: ClassName,
- dbField: FieldSpec,
+ daoName: XClassName,
+ daoImplName: XClassName,
+ dbProperty: XPropertySpec,
scope: CodeGenScope
)
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
index 373864d..9c21f73 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/result/TransactionMethodAdapter.kt
@@ -16,14 +16,12 @@
package androidx.room.solver.transaction.result
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.ext.DEFAULT_IMPLS_CLASS_NAME
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.T
import androidx.room.solver.CodeGenScope
import androidx.room.vo.TransactionMethod
-import com.squareup.javapoet.ClassName
/**
* Class that knows how to generate the transaction method delegate code. Callers should take
@@ -31,64 +29,90 @@
*/
class TransactionMethodAdapter(
private val methodName: String,
+ private val jvmMethodName: String,
private val callType: TransactionMethod.CallType
) {
fun createDelegateToSuperCode(
- returnType: XType,
parameterNames: List<String>,
- daoName: ClassName,
- daoImplName: ClassName,
+ daoName: XClassName,
+ daoImplName: XClassName,
resultVar: String? = null, // name of result var to assign to, null if none
returnStmt: Boolean = false, // true or false to prepend statement with 'return'
scope: CodeGenScope
) {
- scope.builder().apply {
- val params: MutableList<Any> = mutableListOf()
+ scope.builder.apply {
+ val args = mutableListOf<Any>()
val format = buildString {
if (resultVar != null && returnStmt) {
- throw IllegalStateException(
- "Can't assign to var and return in the same statement."
- )
+ error("Can't assign to var and return in the same statement.")
} else if (resultVar != null) {
- append("$T $L = ")
- params.add(returnType.typeName)
- params.add(resultVar)
+ append("%L = ")
+ args.add(resultVar)
} else if (returnStmt) {
append("return ")
}
- when (callType) {
- TransactionMethod.CallType.CONCRETE,
- TransactionMethod.CallType.INHERITED_DEFAULT_JAVA8 -> {
- append("$T.super.$N(")
- params.add(daoImplName)
- params.add(methodName)
- }
- TransactionMethod.CallType.DEFAULT_JAVA8 -> {
- append("$T.super.$N(")
- params.add(daoName)
- params.add(methodName)
- }
- TransactionMethod.CallType.DEFAULT_KOTLIN -> {
- append("$T.$N.$N($T.this")
- params.add(daoName)
- params.add(DEFAULT_IMPLS_CLASS_NAME)
- params.add(methodName)
- params.add(daoImplName)
- }
+
+ val invokeExpr = when (scope.language) {
+ CodeLanguage.JAVA -> scope.getJavaInvokeExpr(daoName, daoImplName)
+ CodeLanguage.KOTLIN -> scope.getKotlinInvokeExpr(daoImplName)
}
- var first = callType != TransactionMethod.CallType.DEFAULT_KOTLIN
- parameterNames.forEach {
- if (first) {
- first = false
- } else {
+ append("%L")
+ args.add(invokeExpr)
+
+ if (scope.language == CodeLanguage.JAVA &&
+ callType == TransactionMethod.CallType.DEFAULT_KOTLIN &&
+ parameterNames.isNotEmpty()
+ ) {
+ // An invoke to DefaultImpls has an extra 1st param so we need a comma if there
+ // are more params.
+ append(", ")
+ }
+ parameterNames.forEachIndexed { i, param ->
+ append("%L")
+ args.add(param)
+ if (i < parameterNames.size - 1) {
append(", ")
}
- append(L)
- params.add(it)
}
append(")")
}
- add(format, *params.toTypedArray())
+ add(format, *args.toTypedArray())
}
}
+
+ private fun CodeGenScope.getJavaInvokeExpr(
+ daoName: XClassName,
+ daoImplName: XClassName,
+ ): XCodeBlock = when (callType) {
+ TransactionMethod.CallType.CONCRETE,
+ TransactionMethod.CallType.INHERITED_DEFAULT_JAVA8 -> {
+ XCodeBlock.of(
+ language,
+ "%T.super.%N(",
+ daoImplName, jvmMethodName
+ )
+ }
+ TransactionMethod.CallType.DEFAULT_JAVA8 -> {
+ XCodeBlock.of(
+ language,
+ "%T.super.%N(",
+ daoName, jvmMethodName
+ )
+ }
+ TransactionMethod.CallType.DEFAULT_KOTLIN -> {
+ XCodeBlock.of(
+ language,
+ "%T.%N.%N(%T.this",
+ daoName, DEFAULT_IMPLS_CLASS_NAME, jvmMethodName, daoImplName
+ )
+ }
+ }
+
+ private fun CodeGenScope.getKotlinInvokeExpr(
+ daoImplName: XClassName,
+ ): XCodeBlock = XCodeBlock.of(
+ language,
+ "super@%T.%N(",
+ daoImplName, methodName
+ )
}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
index 084f28a..6efbe4b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/SingleStatementTypeConverter.kt
@@ -19,7 +19,6 @@
import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.processing.XType
import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.CodeBlock
/**
* A [TypeConverter] that has only 1 statement (e.g. foo ? bar : baz).
@@ -47,7 +46,7 @@
}
/**
- * Returns a [CodeBlock] that will compute the [to] value.
+ * Returns a [XCodeBlock] that will compute the [to] value.
*/
abstract fun buildStatement(
inputVarName: String,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index a7ff843..4d378b7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -28,7 +28,6 @@
import androidx.room.compiler.codegen.XTypeSpec
import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
import androidx.room.compiler.codegen.asClassName
-import androidx.room.compiler.codegen.toJavaPoet
import androidx.room.compiler.processing.XElement
import androidx.room.compiler.processing.XMethodElement
import androidx.room.compiler.processing.XType
@@ -59,9 +58,6 @@
import androidx.room.vo.UpdateMethod
import androidx.room.vo.UpsertionMethod
import androidx.room.vo.WriteQueryMethod
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.TypeSpec
-import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
import java.util.Arrays
import java.util.Collections
import java.util.Locale
@@ -232,13 +228,7 @@
).apply {
returns(
List::class.asClassName().parametrizedBy(
- Class::class.asClassName().parametrizedBy(
- // TODO(b/249984508): Create XTypeName factory for type variable names
- XTypeName(
- java = JWildcardTypeName.subtypeOf(Object::class.java),
- kotlin = com.squareup.kotlinpoet.STAR
- )
- )
+ Class::class.asClassName().parametrizedBy(XTypeName.ANY_WILDCARD)
)
)
addCode(body)
@@ -300,9 +290,9 @@
method.methodBinder.executeAndReturn(
returnType = method.returnType,
parameterNames = method.parameterNames,
- daoName = dao.typeName.toJavaPoet(),
- daoImplName = dao.implTypeName.toJavaPoet(),
- dbField = dbProperty.toJavaPoet(),
+ daoName = dao.typeName,
+ daoImplName = dao.implTypeName,
+ dbProperty = dbProperty,
scope = scope
)
return overrideWithoutAnnotations(method.element, declaredDao)
@@ -474,7 +464,7 @@
private fun <T : DeleteOrUpdateShortcutMethod> createShortcutMethods(
methods: List<T>,
methodPrefix: String,
- implCallback: (T, ShortcutEntity) -> TypeSpec
+ implCallback: (T, ShortcutEntity) -> XTypeSpec
): List<PreparedStmtQuery> {
return methods.mapNotNull { method ->
val entities = method.entities
@@ -503,7 +493,7 @@
private fun createDeleteOrUpdateMethodBody(
method: DeleteOrUpdateShortcutMethod,
- adapters: Map<String, Pair<XPropertySpec, TypeSpec>>
+ adapters: Map<String, Pair<XPropertySpec, XTypeSpec>>
): XCodeBlock {
if (adapters.isEmpty() || method.methodBinder == null) {
return XCodeBlock.builder(codeLanguage).build()
@@ -513,7 +503,7 @@
method.methodBinder.convertAndReturn(
parameters = method.parameters,
adapters = adapters,
- dbField = dbProperty.toJavaPoet(),
+ dbProperty = dbProperty,
scope = scope
)
return scope.generate()
@@ -545,7 +535,7 @@
private fun createUpsertionMethodBody(
method: UpsertionMethod,
- upsertionAdapters: Map<String, Pair<XPropertySpec, CodeBlock>>
+ upsertionAdapters: Map<String, Pair<XPropertySpec, XCodeBlock>>
): XCodeBlock {
if (upsertionAdapters.isEmpty() || method.methodBinder == null) {
return XCodeBlock.builder(codeLanguage).build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
index 1a7220b..aeb8f5b2 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityDeletionAdapterWriter.kt
@@ -16,26 +16,22 @@
package androidx.room.writer
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
-import androidx.room.ext.S
-import androidx.room.ext.SupportDbTypeNames.SQLITE_STMT
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
import androidx.room.solver.CodeGenScope
import androidx.room.vo.FieldWithIndex
import androidx.room.vo.Fields
import androidx.room.vo.ShortcutEntity
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
class EntityDeletionAdapterWriter private constructor(
val tableName: String,
- val pojoTypeName: TypeName,
+ val pojoTypeName: XTypeName,
val fields: Fields
) {
companion object {
@@ -49,54 +45,50 @@
}
return EntityDeletionAdapterWriter(
tableName = entity.tableName,
- pojoTypeName = entity.pojo.typeName.toJavaPoet(),
+ pojoTypeName = entity.pojo.typeName,
fields = fieldsToUse
)
}
}
- fun createAnonymous(typeWriter: TypeWriter, dbParam: String): TypeSpec {
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
- superclass(
- ParameterizedTypeName.get(
- DELETE_OR_UPDATE_ADAPTER.toJavaPoet(),
- pojoTypeName
- )
- )
- addMethod(
- MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- returns(ClassName.get("java.lang", "String"))
- addModifiers(PUBLIC)
+ fun createAnonymous(typeWriter: TypeWriter, dbParam: String): XTypeSpec {
+ return XTypeSpec.anonymousClassBuilder(
+ typeWriter.codeLanguage, "%L", dbParam
+ ).apply {
+ superclass(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER.parametrizedBy(pojoTypeName))
+ addFunction(
+ XFunSpec.builder(
+ language = language,
+ name = "createQuery",
+ visibility = VisibilityModifier.PUBLIC,
+ isOverride = true
+ ).apply {
+ returns(String::class.asClassName())
val query = "DELETE FROM `$tableName` WHERE " +
fields.columnNames.joinToString(" AND ") { "`$it` = ?" }
- addStatement("return $S", query)
+ addStatement("return %S", query)
}.build()
)
- addMethod(
- MethodSpec.methodBuilder("bind").apply {
- val bindScope = CodeGenScope(typeWriter)
- addAnnotation(Override::class.java)
- val stmtParam = "stmt"
- addParameter(
- ParameterSpec.builder(
- SQLITE_STMT.toJavaPoet(),
- stmtParam
- ).build()
- )
- val valueParam = "value"
- addParameter(ParameterSpec.builder(pojoTypeName, valueParam).build())
- returns(TypeName.VOID)
- addModifiers(PUBLIC)
+ addFunction(
+ XFunSpec.builder(
+ language = language,
+ name = "bind",
+ visibility = VisibilityModifier.PUBLIC,
+ isOverride = true
+ ).apply {
+ val stmtParam = "statement"
+ addParameter(SupportDbTypeNames.SQLITE_STMT, stmtParam)
+ val entityParam = "entity"
+ addParameter(pojoTypeName, entityParam)
val mapped = FieldWithIndex.byOrder(fields)
+ val bindScope = CodeGenScope(typeWriter)
FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
+ ownerVar = entityParam,
stmtParamVar = stmtParam,
fieldsWithIndices = mapped,
scope = bindScope
)
- addCode(bindScope.builder().build())
+ addCode(bindScope.generate())
}.build()
)
}.build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
index 24984c9..fc17282 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpdateAdapterWriter.kt
@@ -16,24 +16,19 @@
package androidx.room.writer
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.DELETE_OR_UPDATE_ADAPTER
-import androidx.room.ext.S
-import androidx.room.ext.SupportDbTypeNames.SQLITE_STMT
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.ext.RoomTypeNames
+import androidx.room.ext.SupportDbTypeNames
import androidx.room.solver.CodeGenScope
import androidx.room.vo.FieldWithIndex
import androidx.room.vo.Fields
import androidx.room.vo.Pojo
import androidx.room.vo.ShortcutEntity
import androidx.room.vo.columnNames
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import javax.lang.model.element.Modifier.PUBLIC
class EntityUpdateAdapterWriter private constructor(
val tableName: String,
@@ -51,20 +46,17 @@
)
}
- fun createAnonymous(typeWriter: TypeWriter, dbParam: String): TypeSpec {
- @Suppress("RemoveSingleExpressionStringTemplate")
- return TypeSpec.anonymousClassBuilder("$L", dbParam).apply {
- superclass(
- ParameterizedTypeName.get(
- DELETE_OR_UPDATE_ADAPTER.toJavaPoet(),
- pojo.typeName.toJavaPoet()
- )
- )
- addMethod(
- MethodSpec.methodBuilder("createQuery").apply {
- addAnnotation(Override::class.java)
- addModifiers(PUBLIC)
- returns(ClassName.get("java.lang", "String"))
+ fun createAnonymous(typeWriter: TypeWriter, dbParam: String): XTypeSpec {
+ return XTypeSpec.anonymousClassBuilder(typeWriter.codeLanguage, "%L", dbParam).apply {
+ superclass(RoomTypeNames.DELETE_OR_UPDATE_ADAPTER.parametrizedBy(pojo.typeName))
+ addFunction(
+ XFunSpec.builder(
+ language = language,
+ name = "createQuery",
+ visibility = VisibilityModifier.PUBLIC,
+ isOverride = true
+ ).apply {
+ returns(String::class.asClassName())
val pojoCols = pojo.columnNames.joinToString(",") {
"`$it` = ?"
}
@@ -81,29 +73,24 @@
append(" WHERE")
append(" $pkFieldsCols")
}
- addStatement("return $S", query)
+ addStatement("return %S", query)
}.build()
)
- addMethod(
- MethodSpec.methodBuilder("bind").apply {
- val bindScope = CodeGenScope(typeWriter)
- addAnnotation(Override::class.java)
- addModifiers(PUBLIC)
- returns(TypeName.VOID)
- val stmtParam = "stmt"
- addParameter(
- ParameterSpec.builder(
- SQLITE_STMT.toJavaPoet(),
- stmtParam
- ).build()
- )
- val valueParam = "value"
- addParameter(
- ParameterSpec.builder(pojo.typeName.toJavaPoet(), valueParam).build()
- )
+ addFunction(
+ XFunSpec.builder(
+ language = language,
+ name = "bind",
+ visibility = VisibilityModifier.PUBLIC,
+ isOverride = true
+ ).apply {
+ val stmtParam = "statement"
+ addParameter(SupportDbTypeNames.SQLITE_STMT, stmtParam)
+ val entityParam = "entity"
+ addParameter(pojo.typeName, entityParam)
val mappedField = FieldWithIndex.byOrder(pojo.fields)
+ val bindScope = CodeGenScope(typeWriter)
FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
+ ownerVar = entityParam,
stmtParamVar = stmtParam,
fieldsWithIndices = mappedField,
scope = bindScope
@@ -117,12 +104,12 @@
)
}
FieldReadWriteWriter.bindToStatement(
- ownerVar = valueParam,
+ ownerVar = entityParam,
stmtParamVar = stmtParam,
fieldsWithIndices = mappedPrimaryKeys,
scope = bindScope
)
- addCode(bindScope.builder().build())
+ addCode(bindScope.generate())
}.build()
)
}.build()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
index 35e65c0..7098ed2 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/EntityUpsertionAdapterWriter.kt
@@ -16,17 +16,12 @@
package androidx.room.writer
+import androidx.room.compiler.codegen.XCodeBlock
import androidx.room.compiler.codegen.XPropertySpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.UPSERTION_ADAPTER
-import androidx.room.ext.T
+import androidx.room.ext.RoomTypeNames
import androidx.room.vo.Pojo
import androidx.room.vo.ShortcutEntity
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterizedTypeName
-// TODO b/240736981 need to implement the writer's test
class EntityUpsertionAdapterWriter private constructor(
val tableName: String,
val pojo: Pojo
@@ -44,16 +39,17 @@
entity: ShortcutEntity,
typeWriter: TypeWriter,
dbProperty: XPropertySpec
- ): CodeBlock {
- val upsertionAdapter = ParameterizedTypeName.get(
- UPSERTION_ADAPTER.toJavaPoet(), pojo.typeName.toJavaPoet()
- )
+ ): XCodeBlock {
+ val upsertionAdapter = RoomTypeNames.UPSERTION_ADAPTER.parametrizedBy(pojo.typeName)
val insertionHelper = EntityInsertionAdapterWriter.create(entity, "")
- .createAnonymous(typeWriter, dbProperty).toJavaPoet()
+ .createAnonymous(typeWriter, dbProperty)
val updateHelper = EntityUpdateAdapterWriter.create(entity, "")
.createAnonymous(typeWriter, dbProperty.name)
- return CodeBlock.builder().add("new $T($L, $L)",
- upsertionAdapter, insertionHelper, updateHelper
- ).build()
+ return XCodeBlock.ofNewInstance(
+ language = typeWriter.codeLanguage,
+ typeName = upsertionAdapter,
+ argsFormat = "%L, %L",
+ args = arrayOf(insertionHelper, updateHelper)
+ )
}
}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index 3d43d33..84a98ce 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -37,7 +37,6 @@
import androidx.room.ext.RoomPagingTypeNames
import androidx.room.ext.RoomRxJava2TypeNames
import androidx.room.ext.RoomRxJava3TypeNames
-import androidx.room.ext.RoomTypeNames
import androidx.room.ext.RxJava2TypeNames
import androidx.room.ext.RxJava3TypeNames
import androidx.room.processor.DatabaseViewProcessor
@@ -305,10 +304,7 @@
}
val ROOM_DATABASE_KTX by lazy {
- loadJavaCode(
- "common/input/RoomDatabaseKt.java",
- RoomTypeNames.ROOM_DB_KT.toString()
- )
+ loadKotlinCode("common/input/RoomDatabaseExt.kt")
}
}
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
index 8be0ffb..49fa7f1 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
@@ -755,6 +755,205 @@
)
}
+ @Test
+ fun transactionMethodAdapter_interface(
+ @TestParameter("DISABLE", "ALL_COMPATIBILITY", "ALL_INCOMPATIBLE")
+ jvmDefaultMode: JvmDefaultMode
+ ) {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+ import androidx.sqlite.db.SupportSQLiteQuery
+
+ interface BaseDao {
+ @Transaction
+ open fun baseConcrete() {
+ }
+
+ @Transaction
+ open suspend fun baseSuspendConcrete() {
+ }
+ }
+
+ @Dao
+ interface MyDao : BaseDao {
+ @Transaction
+ fun concrete() {
+ }
+
+ @Transaction
+ fun concreteWithReturn(): String {
+ TODO("")
+ }
+
+ @Transaction
+ fun concreteWithParamsAndReturn(text: String, num: Long): String {
+ TODO("")
+ }
+
+ @Transaction
+ fun concreteWithFunctionalParam(block: () -> Unit) {
+ }
+
+ @Transaction
+ suspend fun suspendConcrete() {
+
+ }
+
+ @Transaction
+ suspend fun suspendConcreteWithReturn(): String {
+ TODO("")
+ }
+
+ @Transaction
+ suspend fun suspendConcreteWithSuspendFunctionalParam(block: suspend () -> Unit) {
+ }
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Long,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM, COMMON.ROOM_DATABASE_KTX),
+ expectedFilePath = getTestGoldenPath(testName),
+ jvmDefaultMode = jvmDefaultMode
+ )
+ }
+
+ @Test
+ fun transactionMethodAdapter_abstractClass() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+ import androidx.sqlite.db.SupportSQLiteQuery
+
+ interface BaseDao {
+ @Transaction
+ open fun baseConcrete() {
+ }
+
+ @Transaction
+ open suspend fun baseSuspendConcrete() {
+ }
+ }
+
+ @Dao
+ abstract class MyDao : BaseDao {
+ @Transaction
+ open fun concrete() {
+ }
+
+ @Transaction
+ open fun concreteInternal() {
+ }
+
+ @Transaction
+ open suspend fun suspendConcrete() {
+
+ }
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Long,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc, COMMON.COROUTINES_ROOM, COMMON.ROOM_DATABASE_KTX),
+ expectedFilePath = getTestGoldenPath(testName),
+ )
+ }
+
+ @Test
+ fun deleteOrUpdateMethodAdapter() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Dao
+ interface MyDao {
+ @Update
+ fun updateEntity(item: MyEntity)
+
+ @Delete
+ fun deleteEntity(item: MyEntity)
+
+ @Update
+ fun updateEntityAndReturnCount(item: MyEntity): Int
+
+ @Delete
+ fun deleteEntityAndReturnCount(item: MyEntity): Int
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Long,
+ val data: String,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
+ @Test
+ fun insertOrUpsertMethodAdapter() {
+ val testName = object {}.javaClass.enclosingMethod!!.name
+ val src = Source.kotlin(
+ "MyDao.kt",
+ """
+ import androidx.room.*
+
+ @Dao
+ interface MyDao {
+ @Insert
+ fun insertEntity(item: MyEntity)
+
+ @Upsert
+ fun upsertEntity(item: MyEntity)
+
+ @Insert
+ fun insertEntityAndReturnRowId(item: MyEntity): Long
+
+ @Upsert
+ fun upsertEntityAndReturnRowId(item: MyEntity): Long
+
+ @Insert
+ fun insertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long>
+
+ @Upsert
+ fun upsertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long>
+ }
+
+ @Entity
+ data class MyEntity(
+ @PrimaryKey
+ val pk: Long,
+ val data: String,
+ )
+ """.trimIndent()
+ )
+ runTest(
+ sources = listOf(src, databaseSrc),
+ expectedFilePath = getTestGoldenPath(testName)
+ )
+ }
+
private fun getTestGoldenPath(testName: String): String {
return "kotlinCodeGen/$testName.kt"
}
diff --git a/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt b/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt
new file mode 100644
index 0000000..f3036c5
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/RoomDatabaseExt.kt
@@ -0,0 +1,10 @@
+@file:JvmName("RoomDatabaseKt")
+
+package androidx.room
+
+import androidx.room.RoomDatabase
+
+@Suppress("UNUSED_PARAMETER")
+public suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R {
+ TODO("")
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
index 7b158a9..fe908dc 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
@@ -39,7 +39,8 @@
public boolean transactionMethod(final int i, final String s, final long l) {
__db.beginTransaction();
try {
- boolean _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
+ final boolean _result;
+ _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
__db.setTransactionSuccessful();
return _result;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
index 6f39c4d..f8423b9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/DeletionDao.java
@@ -41,44 +41,48 @@
this.__db = __db;
this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `User` WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+ statement.bindLong(1, entity.uid);
}
};
this.__deletionAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `MultiPKeyEntity` WHERE `name` = ? AND `lastName` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
- if (value.name == null) {
- stmt.bindNull(1);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ final MultiPKeyEntity entity) {
+ if (entity.name == null) {
+ statement.bindNull(1);
} else {
- stmt.bindString(1, value.name);
+ statement.bindString(1, entity.name);
}
- if (value.lastName == null) {
- stmt.bindNull(2);
+ if (entity.lastName == null) {
+ statement.bindNull(2);
} else {
- stmt.bindString(2, value.lastName);
+ statement.bindString(2, entity.lastName);
}
}
};
this.__deletionAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `Book` WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+ statement.bindLong(1, entity.bookId);
}
};
this.__preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
@@ -142,7 +146,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -156,7 +160,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -170,8 +174,8 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user1);
- _total +=__deletionAdapterOfUser.handleMultiple(others);
+ _total += __deletionAdapterOfUser.handle(user1);
+ _total += __deletionAdapterOfUser.handleMultiple(others);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -185,7 +189,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handleMultiple(users);
+ _total += __deletionAdapterOfUser.handleMultiple(users);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -218,7 +222,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -236,7 +240,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -252,7 +256,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfMultiPKeyEntity.handle(entity);
+ _total += __deletionAdapterOfMultiPKeyEntity.handle(entity);
__db.setTransactionSuccessful();
return _total;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
index 0a7c4ef..daa15e6 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpdateDao.java
@@ -41,91 +41,96 @@
this.__db = __db;
this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- if (value.name == null) {
- stmt.bindNull(2);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+ statement.bindLong(1, entity.uid);
+ if (entity.name == null) {
+ statement.bindNull(2);
} else {
- stmt.bindString(2, value.name);
+ statement.bindString(2, entity.name);
}
- if (value.getLastName() == null) {
- stmt.bindNull(3);
+ if (entity.getLastName() == null) {
+ statement.bindNull(3);
} else {
- stmt.bindString(3, value.getLastName());
+ statement.bindString(3, entity.getLastName());
}
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
};
this.__updateAdapterOfUser_1 = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- if (value.name == null) {
- stmt.bindNull(2);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+ statement.bindLong(1, entity.uid);
+ if (entity.name == null) {
+ statement.bindNull(2);
} else {
- stmt.bindString(2, value.name);
+ statement.bindString(2, entity.name);
}
- if (value.getLastName() == null) {
- stmt.bindNull(3);
+ if (entity.getLastName() == null) {
+ statement.bindNull(3);
} else {
- stmt.bindString(3, value.getLastName());
+ statement.bindString(3, entity.getLastName());
}
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
};
this.__updateAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `MultiPKeyEntity` SET `name` = ?,`lastName` = ? WHERE `name` = ? AND `lastName` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
- if (value.name == null) {
- stmt.bindNull(1);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ final MultiPKeyEntity entity) {
+ if (entity.name == null) {
+ statement.bindNull(1);
} else {
- stmt.bindString(1, value.name);
+ statement.bindString(1, entity.name);
}
- if (value.lastName == null) {
- stmt.bindNull(2);
+ if (entity.lastName == null) {
+ statement.bindNull(2);
} else {
- stmt.bindString(2, value.lastName);
+ statement.bindString(2, entity.lastName);
}
- if (value.name == null) {
- stmt.bindNull(3);
+ if (entity.name == null) {
+ statement.bindNull(3);
} else {
- stmt.bindString(3, value.name);
+ statement.bindString(3, entity.name);
}
- if (value.lastName == null) {
- stmt.bindNull(4);
+ if (entity.lastName == null) {
+ statement.bindNull(4);
} else {
- stmt.bindString(4, value.lastName);
+ statement.bindString(4, entity.lastName);
}
}
};
this.__updateAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
- stmt.bindLong(2, value.uid);
- stmt.bindLong(3, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+ statement.bindLong(1, entity.bookId);
+ statement.bindLong(2, entity.uid);
+ statement.bindLong(3, entity.bookId);
}
};
this.__preparedStmtOfAgeUserByUid = new SharedSQLiteStatement(__db) {
@@ -202,7 +207,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -216,8 +221,8 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user1);
- _total +=__updateAdapterOfUser.handleMultiple(others);
+ _total += __updateAdapterOfUser.handle(user1);
+ _total += __updateAdapterOfUser.handleMultiple(others);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -231,7 +236,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handleMultiple(users);
+ _total += __updateAdapterOfUser.handleMultiple(users);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -245,7 +250,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -278,7 +283,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -296,7 +301,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -312,7 +317,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfMultiPKeyEntity.handle(entity);
+ _total += __updateAdapterOfMultiPKeyEntity.handle(entity);
__db.setTransactionSuccessful();
return _total;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
index d11d549..1e7255b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/UpsertDao.java
@@ -49,25 +49,26 @@
}
}, new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- if (value.name == null) {
- stmt.bindNull(2);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final User entity) {
+ statement.bindLong(1, entity.uid);
+ if (entity.name == null) {
+ statement.bindNull(2);
} else {
- stmt.bindString(2, value.name);
+ statement.bindString(2, entity.name);
}
- if (value.getLastName() == null) {
- stmt.bindNull(3);
+ if (entity.getLastName() == null) {
+ statement.bindNull(3);
} else {
- stmt.bindString(3, value.getLastName());
+ statement.bindString(3, entity.getLastName());
}
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
});
this.__upsertionAdapterOfBook = new EntityUpsertionAdapter<Book>(new EntityInsertionAdapter<Book>(__db) {
@@ -84,15 +85,16 @@
}
}, new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
- stmt.bindLong(2, value.uid);
- stmt.bindLong(3, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement, final Book entity) {
+ statement.bindLong(1, entity.bookId);
+ statement.bindLong(2, entity.uid);
+ statement.bindLong(3, entity.bookId);
}
});
}
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 8a7f450..875dfea 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -39,7 +39,8 @@
public boolean transactionMethod(final int i, final String s, final long l) {
__db.beginTransaction();
try {
- boolean _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
+ final boolean _result;
+ _result = ComplexDao_Impl.super.transactionMethod(i, s, l);
__db.setTransactionSuccessful();
return _result;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
index 732aa17..cb765e9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/DeletionDao.java
@@ -41,36 +41,42 @@
this.__db = __db;
this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `User` WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final User entity) {
+ statement.bindLong(1, entity.uid);
}
};
this.__deletionAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `MultiPKeyEntity` WHERE `name` = ? AND `lastName` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
- stmt.bindString(1, value.name);
- stmt.bindString(2, value.lastName);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final MultiPKeyEntity entity) {
+ statement.bindString(1, entity.name);
+ statement.bindString(2, entity.lastName);
}
};
this.__deletionAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "DELETE FROM `Book` WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final Book entity) {
+ statement.bindLong(1, entity.bookId);
}
};
this.__preparedStmtOfDeleteByUid = new SharedSQLiteStatement(__db) {
@@ -134,7 +140,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -148,7 +154,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -162,8 +168,8 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user1);
- _total +=__deletionAdapterOfUser.handleMultiple(others);
+ _total += __deletionAdapterOfUser.handle(user1);
+ _total += __deletionAdapterOfUser.handleMultiple(others);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -177,7 +183,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handleMultiple(users);
+ _total += __deletionAdapterOfUser.handleMultiple(users);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -210,7 +216,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -228,7 +234,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfUser.handle(user);
+ _total += __deletionAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -244,7 +250,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__deletionAdapterOfMultiPKeyEntity.handle(entity);
+ _total += __deletionAdapterOfMultiPKeyEntity.handle(entity);
__db.setTransactionSuccessful();
return _total;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
index e168f94..bfa4f3b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpdateDao.java
@@ -41,59 +41,67 @@
this.__db = __db;
this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- stmt.bindString(2, value.name);
- stmt.bindString(3, value.getLastName());
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final User entity) {
+ statement.bindLong(1, entity.uid);
+ statement.bindString(2, entity.name);
+ statement.bindString(3, entity.getLastName());
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
};
this.__updateAdapterOfUser_1 = new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- stmt.bindString(2, value.name);
- stmt.bindString(3, value.getLastName());
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final User entity) {
+ statement.bindLong(1, entity.uid);
+ statement.bindString(2, entity.name);
+ statement.bindString(3, entity.getLastName());
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
};
this.__updateAdapterOfMultiPKeyEntity = new EntityDeletionOrUpdateAdapter<MultiPKeyEntity>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `MultiPKeyEntity` SET `name` = ?,`lastName` = ? WHERE `name` = ? AND `lastName` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, MultiPKeyEntity value) {
- stmt.bindString(1, value.name);
- stmt.bindString(2, value.lastName);
- stmt.bindString(3, value.name);
- stmt.bindString(4, value.lastName);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final MultiPKeyEntity entity) {
+ statement.bindString(1, entity.name);
+ statement.bindString(2, entity.lastName);
+ statement.bindString(3, entity.name);
+ statement.bindString(4, entity.lastName);
}
};
this.__updateAdapterOfBook = new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE OR ABORT `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
- stmt.bindLong(2, value.uid);
- stmt.bindLong(3, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final Book entity) {
+ statement.bindLong(1, entity.bookId);
+ statement.bindLong(2, entity.uid);
+ statement.bindLong(3, entity.bookId);
}
};
this.__preparedStmtOfAgeUserByUid = new SharedSQLiteStatement(__db) {
@@ -170,7 +178,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -184,8 +192,8 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user1);
- _total +=__updateAdapterOfUser.handleMultiple(others);
+ _total += __updateAdapterOfUser.handle(user1);
+ _total += __updateAdapterOfUser.handleMultiple(others);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -199,7 +207,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handleMultiple(users);
+ _total += __updateAdapterOfUser.handleMultiple(users);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -213,7 +221,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -246,7 +254,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -264,7 +272,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfUser.handle(user);
+ _total += __updateAdapterOfUser.handle(user);
__db.setTransactionSuccessful();
return _total;
} finally {
@@ -280,7 +288,7 @@
int _total = 0;
__db.beginTransaction();
try {
- _total +=__updateAdapterOfMultiPKeyEntity.handle(entity);
+ _total += __updateAdapterOfMultiPKeyEntity.handle(entity);
__db.setTransactionSuccessful();
return _total;
} finally {
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
index b3d7f2a..8a1374c 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/UpsertDao.java
@@ -42,17 +42,19 @@
}
}, new EntityDeletionOrUpdateAdapter<User>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `User` SET `uid` = ?,`name` = ?,`lastName` = ?,`ageColumn` = ? WHERE `uid` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, User value) {
- stmt.bindLong(1, value.uid);
- stmt.bindString(2, value.name);
- stmt.bindString(3, value.getLastName());
- stmt.bindLong(4, value.age);
- stmt.bindLong(5, value.uid);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final User entity) {
+ statement.bindLong(1, entity.uid);
+ statement.bindString(2, entity.name);
+ statement.bindString(3, entity.getLastName());
+ statement.bindLong(4, entity.age);
+ statement.bindLong(5, entity.uid);
}
});
this.__upsertionAdapterOfBook = new EntityUpsertionAdapter<Book>(new EntityInsertionAdapter<Book>(__db) {
@@ -70,15 +72,17 @@
}
}, new EntityDeletionOrUpdateAdapter<Book>(__db) {
@Override
+ @NonNull
public String createQuery() {
return "UPDATE `Book` SET `bookId` = ?,`uid` = ? WHERE `bookId` = ?";
}
@Override
- public void bind(SupportSQLiteStatement stmt, Book value) {
- stmt.bindLong(1, value.bookId);
- stmt.bindLong(2, value.uid);
- stmt.bindLong(3, value.bookId);
+ public void bind(@NonNull final SupportSQLiteStatement statement,
+ @NonNull final Book entity) {
+ statement.bindLong(1, entity.bookId);
+ statement.bindLong(2, entity.uid);
+ statement.bindLong(3, entity.bookId);
}
});
}
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
new file mode 100644
index 0000000..da2116d4
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
@@ -0,0 +1,95 @@
+import androidx.room.EntityDeletionOrUpdateAdapter
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+ private val __db: RoomDatabase
+
+ private val __deletionAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
+
+ private val __updateAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
+
+ public constructor(__db: RoomDatabase) {
+ this.__db = __db
+ this.__deletionAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String = "DELETE FROM `MyEntity` WHERE `pk` = ?"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.pk)
+ }
+ }
+ this.__updateAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String =
+ "UPDATE OR ABORT `MyEntity` SET `pk` = ?,`data` = ? WHERE `pk` = ?"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.pk)
+ statement.bindString(2, entity.data)
+ statement.bindLong(3, entity.pk)
+ }
+ }
+ }
+
+ public override fun deleteEntity(item: MyEntity): Unit {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ __deletionAdapterOfMyEntity.handle(item)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun deleteEntityAndReturnCount(item: MyEntity): Int {
+ __db.assertNotSuspendingTransaction()
+ var _total: Int = 0
+ __db.beginTransaction()
+ try {
+ _total += __deletionAdapterOfMyEntity.handle(item)
+ __db.setTransactionSuccessful()
+ return _total
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun updateEntity(item: MyEntity): Unit {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ __updateAdapterOfMyEntity.handle(item)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun updateEntityAndReturnCount(item: MyEntity): Int {
+ __db.assertNotSuspendingTransaction()
+ var _total: Int = 0
+ __db.beginTransaction()
+ try {
+ _total += __updateAdapterOfMyEntity.handle(item)
+ __db.setTransactionSuccessful()
+ return _total
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
new file mode 100644
index 0000000..7e60516
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
@@ -0,0 +1,130 @@
+import androidx.room.EntityDeletionOrUpdateAdapter
+import androidx.room.EntityInsertionAdapter
+import androidx.room.EntityUpsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+ private val __db: RoomDatabase
+
+ private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+
+ private val __upsertionAdapterOfMyEntity: EntityUpsertionAdapter<MyEntity>
+
+ public constructor(__db: RoomDatabase) {
+ this.__db = __db
+ this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String =
+ "INSERT OR ABORT INTO `MyEntity` (`pk`,`data`) VALUES (?,?)"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.pk)
+ statement.bindString(2, entity.data)
+ }
+ }
+ this.__upsertionAdapterOfMyEntity = EntityUpsertionAdapter<MyEntity>(object :
+ EntityInsertionAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String =
+ "INSERT INTO `MyEntity` (`pk`,`data`) VALUES (?,?)"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.pk)
+ statement.bindString(2, entity.data)
+ }
+ }, object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
+ public override fun createQuery(): String =
+ "UPDATE `MyEntity` SET `pk` = ?,`data` = ? WHERE `pk` = ?"
+
+ public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+ statement.bindLong(1, entity.pk)
+ statement.bindString(2, entity.data)
+ statement.bindLong(3, entity.pk)
+ }
+ })
+ }
+
+ public override fun insertEntity(item: MyEntity): Unit {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ __insertionAdapterOfMyEntity.insert(item)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun insertEntityAndReturnRowId(item: MyEntity): Long {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ val _result: Long = __insertionAdapterOfMyEntity.insertAndReturnId(item)
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun insertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long> {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ val _result: List<Long> = __insertionAdapterOfMyEntity.insertAndReturnIdsList(items)
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun upsertEntity(item: MyEntity): Unit {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ __upsertionAdapterOfMyEntity.upsert(item)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun upsertEntityAndReturnRowId(item: MyEntity): Long {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ val _result: Long = __upsertionAdapterOfMyEntity.upsertAndReturnId(item)
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun upsertEntityListAndReturnRowIds(items: List<MyEntity>): List<Long> {
+ __db.assertNotSuspendingTransaction()
+ __db.beginTransaction()
+ try {
+ val _result: List<Long> = __upsertionAdapterOfMyEntity.upsertAndReturnIdsList(items)
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
new file mode 100644
index 0000000..3658d37
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -0,0 +1,65 @@
+import androidx.room.RoomDatabase
+import androidx.room.withTransaction
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+ private val __db: RoomDatabase
+
+ public constructor(__db: RoomDatabase) {
+ this.__db = __db
+ }
+
+ public override fun baseConcrete(): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.baseConcrete()
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override suspend fun baseSuspendConcrete(): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.baseSuspendConcrete()
+ }
+ }
+
+ public override fun concrete(): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.concrete()
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun concreteInternal(): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.concreteInternal()
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override suspend fun suspendConcrete(): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.suspendConcrete()
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
new file mode 100644
index 0000000..15e154e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
@@ -0,0 +1,104 @@
+import androidx.room.RoomDatabase
+import androidx.room.withTransaction
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Function0
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.coroutines.SuspendFunction0
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+ private val __db: RoomDatabase
+
+ public constructor(__db: RoomDatabase) {
+ this.__db = __db
+ }
+
+ public override fun baseConcrete(): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.baseConcrete()
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override suspend fun baseSuspendConcrete(): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.baseSuspendConcrete()
+ }
+ }
+
+ public override fun concrete(): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.concrete()
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun concreteWithReturn(): String {
+ __db.beginTransaction()
+ try {
+ val _result: String
+ _result = super@MyDao_Impl.concreteWithReturn()
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun concreteWithParamsAndReturn(text: String, num: Long): String {
+ __db.beginTransaction()
+ try {
+ val _result: String
+ _result = super@MyDao_Impl.concreteWithParamsAndReturn(text, num)
+ __db.setTransactionSuccessful()
+ return _result
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override fun concreteWithFunctionalParam(block: Function0<Unit>): Unit {
+ __db.beginTransaction()
+ try {
+ super@MyDao_Impl.concreteWithFunctionalParam(block)
+ __db.setTransactionSuccessful()
+ } finally {
+ __db.endTransaction()
+ }
+ }
+
+ public override suspend fun suspendConcrete(): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.suspendConcrete()
+ }
+ }
+
+ public override suspend fun suspendConcreteWithReturn(): String = __db.withTransaction {
+ super@MyDao_Impl.suspendConcreteWithReturn()
+ }
+
+ public override suspend
+ fun suspendConcreteWithSuspendFunctionalParam(block: SuspendFunction0<Unit>): Unit {
+ __db.withTransaction {
+ super@MyDao_Impl.suspendConcreteWithSuspendFunctionalParam(block)
+ }
+ }
+
+ public companion object {
+ @JvmStatic
+ public fun getRequiredConverters(): List<Class<*>> = emptyList()
+ }
+}
\ No newline at end of file
diff --git a/security/security-crypto-ktx/build.gradle b/security/security-crypto-ktx/build.gradle
index e676a24..d25904c 100644
--- a/security/security-crypto-ktx/build.gradle
+++ b/security/security-crypto-ktx/build.gradle
@@ -23,7 +23,7 @@
}
dependencies {
- api("androidx.security:security-crypto:1.1.0-alpha03")
+ api(project(":security:security-crypto"))
api(libs.kotlinStdlib)
androidTestImplementation(libs.junit)
diff --git a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
index b21f7e7..9027376 100644
--- a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
+++ b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
@@ -47,15 +47,37 @@
/**
* Class used to create and read encrypted files.
+ * <br />
+ * <br />
+ * <b>WARNING</b>: The encrypted file should not be backed up with Auto Backup. When restoring the
+ * file it is likely the key used to encrypt it will no longer be present. You should exclude all
+ * <code>EncryptedFile</code>s from backup using
+ * <a href="https://developer.android.com/guide/topics/data/autobackup#IncludingFiles">backup rules</a>.
+ * Be aware that if you are not explicitly calling <code>setKeysetPrefName()</code> there is also a
+ * silently-created default preferences file created at
+ * <pre>
+ * ApplicationProvider
+ * .getApplicationContext()
+ * .getFilesDir()
+ * .getParent() + "/shared_prefs/__androidx_security_crypto_encrypted_file_pref__"
+ * </pre>
+ *
+ * This preferences file (or any others created with a custom specified location) also should be
+ * excluded from backups.
+ * <br />
+ * <br />
+ * Basic use of the class:
*
* <pre>
- * String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ * MasterKey masterKey = new MasterKey.Builder(context)
+ * .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ * .build();
*
* File file = new File(context.getFilesDir(), "secret_data");
* EncryptedFile encryptedFile = EncryptedFile.Builder(
- * file,
* context,
- * masterKeyAlias,
+ * file,
+ * masterKey,
* EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
* ).build();
*
diff --git a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
index ad66feb..2060e40 100644
--- a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
+++ b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedSharedPreferences.java
@@ -51,14 +51,25 @@
/**
* An implementation of {@link SharedPreferences} that encrypts keys and values.
+ * <br />
+ * <br />
+ * <b>WARNING</b>: The preference file should not be backed up with Auto Backup. When restoring the
+ * file it is likely the key used to encrypt it will no longer be present. You should exclude all
+ * <code>EncryptedSharedPreference</code>s from backup using
+ * <a href="https://developer.android.com/guide/topics/data/autobackup#IncludingFiles">backup rules</a>.
+ * <br />
+ * <br />
+ * Basic use of the class:
*
* <pre>
- * String masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC);
+ * MasterKey masterKey = new MasterKey.Builder(context)
+ * .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+ * .build();
*
* SharedPreferences sharedPreferences = EncryptedSharedPreferences.create(
- * "secret_shared_prefs",
- * masterKeyAlias,
* context,
+ * "secret_shared_prefs",
+ * masterKey,
* EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
* EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
* );
diff --git a/settings.gradle b/settings.gradle
index 3696086..bddb08f 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -789,6 +789,9 @@
includeProject(":privacysandbox:tools:tools-apipackager", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-core", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-testing", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-client", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-core", [BuildType.MAIN])
+includeProject(":privacysandbox:ui:ui-provider", [BuildType.MAIN])
includeProject(":profileinstaller:profileinstaller", [BuildType.MAIN, BuildType.COMPOSE])
includeProject(":profileinstaller:integration-tests:profile-verification", [BuildType.MAIN])
includeProject(":profileinstaller:integration-tests:profile-verification-sample", [BuildType.MAIN])
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
index c807e23..f419abd 100644
--- a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
@@ -79,4 +79,33 @@
val cursor2 = db.query("select * from user where idk=1")
assertThat(cursor2.count).isEqualTo(0)
}
+
+ @Test
+ fun testFrameWorkSQLiteDatabase_attachDbWorks() {
+ val openHelper2 = FrameworkSQLiteOpenHelper(
+ context,
+ "test2.db",
+ OpenHelperRecoveryTest.EmptyCallback(),
+ useNoBackupDirectory = false,
+ allowDataLossOnRecovery = false
+ )
+ val db1 = openHelper.writableDatabase
+ val db2 = openHelper2.writableDatabase
+
+ db1.execSQL(
+ "ATTACH DATABASE '${db2.path}' AS database2"
+ )
+
+ val cursor = db1.query("pragma database_list")
+ val expected = buildList<Pair<String, String>> {
+ while (cursor.moveToNext()) {
+ add(cursor.getString(1) to cursor.getString(2))
+ }
+ }
+ cursor.close()
+ openHelper2.close()
+
+ val actual = db1.attachedDbs?.map { it.first to it.second }
+ assertThat(expected).isEqualTo(actual)
+ }
}
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
index 00257a1..b9aeb34 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
@@ -290,7 +290,8 @@
override val isWriteAheadLoggingEnabled: Boolean
get() = SupportSQLiteCompat.Api16Impl.isWriteAheadLoggingEnabled(delegate)
- override val attachedDbs: List<Pair<String, String>>? = delegate.attachedDbs
+ override val attachedDbs: List<Pair<String, String>>?
+ get() = delegate.attachedDbs
override val isDatabaseIntegrityOk: Boolean
get() = delegate.isDatabaseIntegrityOk
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
index 1648409..95c305a 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BaseTest.java
@@ -16,6 +16,7 @@
package androidx.test.uiautomator.testapp;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.app.Activity;
@@ -27,10 +28,13 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.Configurator;
import androidx.test.uiautomator.UiDevice;
+import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.Until;
import org.junit.Before;
+import org.junit.function.ThrowingRunnable;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
@@ -62,4 +66,17 @@
assertTrue("Test app not visible after launching activity",
mDevice.wait(Until.hasObject(By.pkg(TEST_APP)), TIMEOUT_MS));
}
+
+ // Helper to verify that an operation throws a UiObjectNotFoundException without waiting for
+ // the full 10s default timeout.
+ protected static void assertUiObjectNotFound(ThrowingRunnable runnable) {
+ Configurator configurator = Configurator.getInstance();
+ long timeout = configurator.getWaitForSelectorTimeout();
+ configurator.setWaitForSelectorTimeout(1_000);
+ try {
+ assertThrows(UiObjectNotFoundException.class, runnable);
+ } finally {
+ configurator.setWaitForSelectorTimeout(timeout);
+ }
+ }
}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index ec2bad0..5459765 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -355,36 +355,56 @@
@Test
public void testSetOrientationLeft() throws Exception {
launchTestActivity(KeycodeTestActivity.class);
-
- assertTrue(mDevice.isNaturalOrientation());
- assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
-
- mDevice.setOrientationLeft();
- // Make the device wait for 1 sec for the rotation animation to finish.
- SystemClock.sleep(1_000);
- assertFalse(mDevice.isNaturalOrientation());
- assertEquals(UiAutomation.ROTATION_FREEZE_90, mDevice.getDisplayRotation());
-
- mDevice.setOrientationNatural();
- SystemClock.sleep(1_000);
- assertTrue(mDevice.isNaturalOrientation());
+ try {
+ assertTrue(mDevice.isNaturalOrientation());
+ assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+ mDevice.setOrientationLeft();
+ // Make the device wait for 1 sec for the rotation animation to finish.
+ SystemClock.sleep(1_000);
+ assertFalse(mDevice.isNaturalOrientation());
+ assertEquals(UiAutomation.ROTATION_FREEZE_90, mDevice.getDisplayRotation());
+ mDevice.setOrientationNatural();
+ SystemClock.sleep(1_000);
+ assertTrue(mDevice.isNaturalOrientation());
+ } finally {
+ mDevice.unfreezeRotation();
+ }
}
@Test
public void testSetOrientationRight() throws Exception {
launchTestActivity(KeycodeTestActivity.class);
+ try {
+ assertTrue(mDevice.isNaturalOrientation());
+ assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+ mDevice.setOrientationRight();
+ SystemClock.sleep(1_000);
+ assertFalse(mDevice.isNaturalOrientation());
+ assertEquals(UiAutomation.ROTATION_FREEZE_270, mDevice.getDisplayRotation());
+ mDevice.setOrientationNatural();
+ SystemClock.sleep(1_000);
+ assertTrue(mDevice.isNaturalOrientation());
+ } finally {
+ mDevice.unfreezeRotation();
+ }
+ }
- assertTrue(mDevice.isNaturalOrientation());
- assertEquals(UiAutomation.ROTATION_FREEZE_0, mDevice.getDisplayRotation());
+ @Test
+ public void testIsScreenOn() throws Exception {
+ launchTestActivity(MainActivity.class);
- mDevice.setOrientationRight();
- SystemClock.sleep(1_000);
- assertFalse(mDevice.isNaturalOrientation());
- assertEquals(UiAutomation.ROTATION_FREEZE_270, mDevice.getDisplayRotation());
+ mDevice.wakeUp();
+ assertTrue(mDevice.isScreenOn());
- mDevice.setOrientationNatural();
- SystemClock.sleep(1_000);
- assertTrue(mDevice.isNaturalOrientation());
+ try {
+ mDevice.sleep();
+ assertFalse(mDevice.isScreenOn());
+ } finally {
+ mDevice.wakeUp();
+ mDevice.pressMenu();
+ assertTrue("Failed to wake up device and remove lockscreen",
+ mDevice.hasObject(By.pkg(TEST_APP)));
+ }
}
@Test
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index 96d56df..64af3dd 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -203,9 +203,9 @@
// Get the same textView object via different methods.
UiObject2 textView1 = mDevice.findObject(By.res(TEST_APP, "example_id"));
UiObject2 textView2 = mDevice.findObject(By.text("TextView with an id"));
- assertTrue(textView1.equals(textView2));
+ assertEquals(textView1, textView2);
UiObject2 linearLayout = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
- assertFalse(textView1.equals(linearLayout));
+ assertNotEquals(textView1, linearLayout);
}
@Test
@@ -598,22 +598,15 @@
launchTestActivity(VerticalScrollTestActivity.class);
assertTrue(mDevice.hasObject(By.res(TEST_APP, "top_text"))); // Initially at top.
- // Scroll down to bottom (20000px) in increments of 5000px.
+ // Scroll down to bottom where is two-screen-height distant from the top.
UiObject2 scrollView = mDevice.findObject(By.res(TEST_APP, "scroll_view"));
scrollView.setGestureMargin(SCROLL_MARGIN); // Avoid touching too close to the edges.
+
Rect bounds = scrollView.getVisibleBounds();
- float percent = 5_000f / (bounds.height() - 2 * SCROLL_MARGIN);
- // Scroll to an element 5000px from the top.
+ float percent =
+ (float) (mDevice.getDisplayHeight() * 2 / (bounds.height() - 2 * SCROLL_MARGIN));
scrollView.scroll(Direction.DOWN, percent);
- assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_5000")));
- // Scroll to an element 10000px from the top.
- scrollView.scroll(Direction.DOWN, percent);
- assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_10000")));
- // Scroll to an element 15000px from the top.
- scrollView.scroll(Direction.DOWN, percent);
- assertTrue(mDevice.hasObject(By.res(TEST_APP, "from_top_15000")));
- // Scroll to the bottom element 20000px from the top.
- scrollView.scroll(Direction.DOWN, percent);
+
assertTrue(mDevice.hasObject(By.res(TEST_APP, "bottom_text")));
}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index de621a2..ee12f2d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -26,7 +26,6 @@
import androidx.test.filters.SdkSuppress;
import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiSelector;
import org.junit.Test;
@@ -75,7 +74,7 @@
UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::getChildCount);
+ assertUiObjectNotFound(noNode::getChildCount);
}
@Test
@@ -297,13 +296,13 @@
UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::click);
- assertThrows(UiObjectNotFoundException.class, noNode::clickAndWaitForNewWindow);
- assertThrows(UiObjectNotFoundException.class, noNode::clickTopLeft);
- assertThrows(UiObjectNotFoundException.class, noNode::longClickBottomRight);
- assertThrows(UiObjectNotFoundException.class, noNode::clickBottomRight);
- assertThrows(UiObjectNotFoundException.class, noNode::longClick);
- assertThrows(UiObjectNotFoundException.class, noNode::longClickTopLeft);
+ assertUiObjectNotFound(noNode::click);
+ assertUiObjectNotFound(noNode::clickAndWaitForNewWindow);
+ assertUiObjectNotFound(noNode::clickTopLeft);
+ assertUiObjectNotFound(noNode::longClickBottomRight);
+ assertUiObjectNotFound(noNode::clickBottomRight);
+ assertUiObjectNotFound(noNode::longClick);
+ assertUiObjectNotFound(noNode::longClickTopLeft);
}
@Test
@@ -383,13 +382,13 @@
UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::getText);
- assertThrows(UiObjectNotFoundException.class, noNode::getClassName);
- assertThrows(UiObjectNotFoundException.class, noNode::getContentDescription);
- assertThrows(UiObjectNotFoundException.class, () -> noNode.legacySetText("new_text"));
- assertThrows(UiObjectNotFoundException.class, () -> noNode.setText("new_text"));
- assertThrows(UiObjectNotFoundException.class, noNode::clearTextField);
- assertThrows(UiObjectNotFoundException.class, noNode::getPackageName);
+ assertUiObjectNotFound(noNode::getText);
+ assertUiObjectNotFound(noNode::getClassName);
+ assertUiObjectNotFound(noNode::getContentDescription);
+ assertUiObjectNotFound(() -> noNode.legacySetText("new_text"));
+ assertUiObjectNotFound(() -> noNode.setText("new_text"));
+ assertUiObjectNotFound(noNode::clearTextField);
+ assertUiObjectNotFound(noNode::getPackageName);
}
@Test
@@ -513,15 +512,15 @@
UiObject noNode = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::isChecked);
- assertThrows(UiObjectNotFoundException.class, noNode::isSelected);
- assertThrows(UiObjectNotFoundException.class, noNode::isCheckable);
- assertThrows(UiObjectNotFoundException.class, noNode::isEnabled);
- assertThrows(UiObjectNotFoundException.class, noNode::isClickable);
- assertThrows(UiObjectNotFoundException.class, noNode::isFocused);
- assertThrows(UiObjectNotFoundException.class, noNode::isFocusable);
- assertThrows(UiObjectNotFoundException.class, noNode::isScrollable);
- assertThrows(UiObjectNotFoundException.class, noNode::isLongClickable);
+ assertUiObjectNotFound(noNode::isChecked);
+ assertUiObjectNotFound(noNode::isSelected);
+ assertUiObjectNotFound(noNode::isCheckable);
+ assertUiObjectNotFound(noNode::isEnabled);
+ assertUiObjectNotFound(noNode::isClickable);
+ assertUiObjectNotFound(noNode::isFocused);
+ assertUiObjectNotFound(noNode::isFocusable);
+ assertUiObjectNotFound(noNode::isScrollable);
+ assertUiObjectNotFound(noNode::isLongClickable);
}
@Test
@@ -671,8 +670,8 @@
UiObject smallArea = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ "/small_area"));
- assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchOut(100, 10));
- assertThrows(UiObjectNotFoundException.class, () -> noNode.pinchIn(100, 10));
+ assertUiObjectNotFound(() -> noNode.pinchOut(100, 10));
+ assertUiObjectNotFound(() -> noNode.pinchIn(100, 10));
assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
}
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
index b30e677..a5b7e7c 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
@@ -18,13 +18,11 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import android.widget.TextView;
import androidx.test.uiautomator.UiObject;
-import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiScrollable;
import androidx.test.uiautomator.UiSelector;
@@ -65,7 +63,7 @@
"This is the bottom");
assertEquals("This is the bottom", target.getText());
- assertThrows(UiObjectNotFoundException.class,
+ assertUiObjectNotFound(
() -> relativeLayout.getChildByDescription(
new UiSelector().className(TextView.class),
"This is non-existent"));
@@ -82,7 +80,7 @@
"This is the top", false);
assertEquals("This is the top", target.getText());
- assertThrows(UiObjectNotFoundException.class,
+ assertUiObjectNotFound(
() -> relativeLayout.getChildByDescription(
new UiSelector().className(TextView.class), "This is the bottom",
false));
@@ -115,7 +113,7 @@
"This is the bottom");
assertEquals("This is the bottom", target.getText());
- assertThrows(UiObjectNotFoundException.class,
+ assertUiObjectNotFound(
() -> relativeLayout.getChildByText(new UiSelector().className(TextView.class),
"This is non-existent"));
}
@@ -131,7 +129,7 @@
"This is the top", false);
assertEquals("This is the top", target.getText());
- assertThrows(UiObjectNotFoundException.class,
+ assertUiObjectNotFound(
() -> relativeLayout.getChildByText(new UiSelector().className(TextView.class),
"This is the bottom", false));
}
@@ -143,10 +141,10 @@
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
- + "/from_top_15000"));
+ + "/bottom_text"));
assertFalse(target.exists());
- assertTrue(relativeLayout.scrollDescriptionIntoView("This is 15000px from the top"));
+ assertTrue(relativeLayout.scrollDescriptionIntoView("This is the bottom"));
assertTrue(target.exists());
assertFalse(relativeLayout.scrollDescriptionIntoView("This is non-existent"));
}
@@ -158,7 +156,7 @@
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
- + "/from_top_15000"));
+ + "/bottom_text"));
UiObject nonExistentTarget = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ "/not_exist"));
@@ -174,7 +172,7 @@
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
- UiSelector target = new UiSelector().resourceId(TEST_APP + ":id/from_top_15000");
+ UiSelector target = new UiSelector().resourceId(TEST_APP + ":id/bottom_text");
UiSelector nonExistentTarget = new UiSelector().resourceId(TEST_APP + ":id/not_exist");
assertFalse(mDevice.findObject(target).exists());
@@ -190,11 +188,11 @@
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
UiObject target = mDevice.findObject(
- new UiSelector().resourceId(TEST_APP + ":id/from_top_15000"));
+ new UiSelector().resourceId(TEST_APP + ":id/bottom_text"));
assertTrue(relativeLayout.scrollIntoView(target));
assertTrue(relativeLayout.ensureFullyVisible(target));
- assertThrows(UiObjectNotFoundException.class,
+ assertUiObjectNotFound(
() -> relativeLayout.ensureFullyVisible(
mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id/no_node"))));
}
@@ -206,10 +204,10 @@
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
UiObject target = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
- + "/from_top_15000"));
+ + "/bottom_text"));
assertFalse(target.exists());
- assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+ assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
assertTrue(target.exists());
assertFalse(relativeLayout.scrollTextIntoView("This is non-existent"));
}
@@ -234,7 +232,7 @@
// Assert throwing exception for `scrollForward(int)` and all its related methods.
UiScrollable noNode = new UiScrollable(new UiSelector().resourceId(TEST_APP + ":id"
+ "/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::flingForward);
+ assertUiObjectNotFound(noNode::flingForward);
}
@Test
@@ -271,7 +269,7 @@
UiScrollable noNode = new UiScrollable(new UiSelector().resourceId(TEST_APP + ":id"
+ "/no_node"));
- assertThrows(UiObjectNotFoundException.class, noNode::flingBackward);
+ assertUiObjectNotFound(noNode::flingBackward);
}
@Test
@@ -305,7 +303,7 @@
UiObject topText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ "/top_text"));
- assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+ assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
assertFalse(topText.exists());
assertTrue(relativeLayout.scrollToBeginning(20, 50));
assertTrue(topText.exists());
@@ -320,7 +318,7 @@
UiObject topText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
+ "/top_text"));
- assertTrue(relativeLayout.scrollTextIntoView("This is 15000px from the top"));
+ assertTrue(relativeLayout.scrollTextIntoView("This is the bottom"));
assertFalse(topText.exists());
assertTrue(relativeLayout.scrollToBeginning(20));
assertTrue(topText.exists());
@@ -354,7 +352,6 @@
@Test
public void testScrollToEnd() throws Exception {
launchTestActivity(VerticalScrollTestActivity.class);
- launchTestActivity(VerticalScrollTestActivity.class);
UiScrollable relativeLayout = new UiScrollable(
new UiSelector().resourceId(TEST_APP + ":id/relative_layout"));
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
index 8f39574..183f9cd 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/VerticalScrollTestActivity.java
@@ -17,10 +17,14 @@
package androidx.test.uiautomator.testapp;
import android.app.Activity;
+import android.graphics.Point;
import android.os.Bundle;
+import android.view.Display;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
import androidx.annotation.Nullable;
@@ -34,6 +38,15 @@
setContentView(R.layout.vertical_scroll_test_activity);
+ // Get the size of the screen.
+ Display display = getWindowManager().getDefaultDisplay();
+ Point displaySize = new Point();
+ display.getSize(displaySize);
+
+ // Set up the scrolling layout whose height is two times of the screen height.
+ RelativeLayout layout = findViewById(R.id.relative_layout);
+ layout.setLayoutParams(new FrameLayout.LayoutParams(displaySize.x, displaySize.y * 2));
+
mGestureDetector = new GestureDetector(this, new SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent event) {
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
index d7d3aee..f2482db 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/vertical_scroll_test_activity.xml
@@ -28,7 +28,7 @@
<RelativeLayout
android:id="@+id/relative_layout"
android:layout_width="fill_parent"
- android:layout_height="20000px">
+ android:layout_height="wrap_content">
<TextView
android:id="@+id/top_text"
@@ -39,33 +39,6 @@
android:text="This is the top" />
<TextView
- android:id="@+id/from_top_5000"
- android:layout_width="wrap_content"
- android:layout_height="50px"
- android:layout_alignParentTop="true"
- android:layout_marginTop="5000px"
- android:contentDescription="This is 5000px from the top"
- android:text="This is 5000px from the top" />
-
- <TextView
- android:id="@+id/from_top_10000"
- android:layout_width="wrap_content"
- android:layout_height="50px"
- android:layout_alignParentTop="true"
- android:layout_marginTop="10000px"
- android:contentDescription="This is 10000px from the top"
- android:text="This is 10000px from the top" />
-
- <TextView
- android:id="@+id/from_top_15000"
- android:layout_width="wrap_content"
- android:layout_height="50px"
- android:layout_alignParentTop="true"
- android:layout_marginTop="15000px"
- android:contentDescription="This is 15000px from the top"
- android:text="This is 15000px from the top" />
-
- <TextView
android:id="@+id/bottom_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
diff --git a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
index 0f9d364..fc46750 100644
--- a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
+++ b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
@@ -178,14 +178,6 @@
}
@Test
- public void testWakeUpAndShutDownScreen() throws Exception {
- mDevice.wakeUp();
- assertTrue(mDevice.isScreenOn());
- mDevice.sleep();
- assertFalse(mDevice.isScreenOn());
- }
-
- @Test
@SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
public void testGetUiAutomation_withoutFlags() {
mDevice.getUiAutomation();
diff --git a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
index edb0f02..ab49a11 100644
--- a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
+++ b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/LocaleTestUtilsTest.kt
@@ -26,8 +26,8 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import org.hamcrest.CoreMatchers
+import org.hamcrest.MatcherAssert.assertThat
import org.junit.After
-import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -85,7 +85,7 @@
val getReason: (String, String) -> String = { name, code ->
"$name test language '$code' does not exist on test device"
}
- Assert.assertThat(
+ assertThat(
getReason(
"Default",
LocaleTestUtils.DEFAULT_TEST_LANGUAGE
@@ -93,12 +93,12 @@
availableLanguages,
CoreMatchers.hasItem(LocaleTestUtils.DEFAULT_TEST_LANGUAGE)
)
- Assert.assertThat(
+ assertThat(
getReason("LTR", LocaleTestUtils.LTR_LANGUAGE),
availableLanguages,
CoreMatchers.hasItem(LocaleTestUtils.LTR_LANGUAGE)
)
- Assert.assertThat(
+ assertThat(
getReason("RTL", LocaleTestUtils.RTL_LANGUAGE),
availableLanguages,
CoreMatchers.hasItem(LocaleTestUtils.RTL_LANGUAGE)
@@ -110,13 +110,13 @@
}
private fun assertLocaleIs(lang: String, expectRtl: Boolean) {
- Assert.assertThat(
+ assertThat(
"Locale should be $lang",
configuration.language,
CoreMatchers.equalTo(lang)
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
- Assert.assertThat(
+ assertThat(
"Layout direction should be ${if (expectRtl) "RTL" else "LTR"}",
configuration.layoutDirection,
CoreMatchers.equalTo(if (expectRtl) LAYOUT_DIRECTION_RTL else LAYOUT_DIRECTION_LTR)
@@ -125,7 +125,7 @@
}
private fun determineDefaultLayoutDirection() {
- Assert.assertThat(
+ assertThat(
"Locale must still be the default when determining the default layout direction",
configuration.language,
CoreMatchers.equalTo(DEFAULT_LANGUAGE)
@@ -134,4 +134,4 @@
expectRtlInDefaultLanguage = configuration.layoutDirection == LAYOUT_DIRECTION_RTL
}
}
-}
\ No newline at end of file
+}
diff --git a/tracing/tracing-perfetto-binary/build.gradle b/tracing/tracing-perfetto-binary/build.gradle
index 11f4655..041c702 100644
--- a/tracing/tracing-perfetto-binary/build.gradle
+++ b/tracing/tracing-perfetto-binary/build.gradle
@@ -78,6 +78,7 @@
androidTestImplementation(libs.kotlinStdlib)
androidTestImplementation(libs.testExtJunit)
androidTestImplementation(libs.testRunner)
+ androidTestImplementation(project(":benchmark:benchmark-common")) // for supported ABI checks
}
androidx {
diff --git a/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt b/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
index 520ff8d..4b4e2b2 100644
--- a/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
+++ b/tracing/tracing-perfetto-binary/src/androidTest/java/androidx/tracing/perfetto/binary/test/TracingTest.kt
@@ -18,14 +18,41 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import dalvik.system.BaseDexClassLoader
+import java.io.File
+import java.util.zip.ZipFile
+import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidJUnit4::class)
class TracingTest {
+ /**
+ * The test verifies that the library was assembled and can be found by the system.
+ * We cannot load the library since it contains explicit JNI method registration tied to
+ * the [androidx.tracing.perfetto.jni.PerfettoNative] class.
+ *
+ * Methods of the library are further tested in e.g.:
+ * - [androidx.tracing.perfetto.jni.test.PerfettoNativeTest]
+ * - [androidx.compose.integration.macrobenchmark.TrivialTracingBenchmark]
+ */
@Test
- fun test_loadLibrary() {
- System.loadLibrary("tracing_perfetto")
+ fun test_library_was_created() {
+ // check that the system can resolve the library
+ val nativeLibraryName = System.mapLibraryName("tracing_perfetto")
+
+ // check that the class loader can find the library
+ val classLoader = javaClass.classLoader as BaseDexClassLoader
+ val libraryPath = classLoader.findLibrary("tracing_perfetto")
+ assertTrue(libraryPath.endsWith("/$nativeLibraryName"))
+
+ // check that the APK contains the library file
+ val context = InstrumentationRegistry.getInstrumentation().context
+ val baseApk = File(context.applicationInfo.publicSourceDir!!)
+ assertTrue(ZipFile(baseApk).entries().asSequence().any {
+ it.name.endsWith("/$nativeLibraryName")
+ })
}
}
diff --git a/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc b/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
index b0c2953..faffb2f 100644
--- a/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
+++ b/tracing/tracing-perfetto-binary/src/main/cpp/jni/androidx_tracing_perfetto_jni_PerfettoNative.cc
@@ -20,13 +20,13 @@
extern "C" {
-JNIEXPORT void JNICALL
+static void JNICALL
Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeRegisterWithPerfetto(
JNIEnv *env, __unused jclass clazz) {
tracing_perfetto::RegisterWithPerfetto();
}
-JNIEXPORT void JNICALL
+static void JNICALL
Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventBegin(
JNIEnv *env, __unused jclass clazz, jint key, jstring traceInfo) {
const char *traceInfoUtf = env->GetStringUTFChars(traceInfo, NULL);
@@ -34,16 +34,68 @@
env->ReleaseStringUTFChars(traceInfo, traceInfoUtf);
}
-JNIEXPORT void JNICALL
-Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd(
- JNIEnv *env, __unused jclass clazz) {
+static void JNICALL
+Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd() {
tracing_perfetto::TraceEventEnd();
}
-JNIEXPORT jstring JNICALL
+static jstring JNICALL
Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeVersion(
JNIEnv *env, __unused jclass clazz) {
return env->NewStringUTF(tracing_perfetto::Version());
}
} // extern "C"
+// Explicitly registering native methods using CriticalNative / FastNative as per:
+// https://source.android.com/devices/tech/dalvik/improvements#faster-native-methods.
+// Note: this applies to Android 8 - 11. In Android 12+, this is recommended (to avoid slow lookups
+// on first use), but not necessary.
+
+static JNINativeMethod sMethods[] = {
+ {"nativeRegisterWithPerfetto",
+ "()V",
+ reinterpret_cast<void *>(
+ Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeRegisterWithPerfetto)
+ },
+ {"nativeTraceEventBegin",
+ "(ILjava/lang/String;)V",
+ reinterpret_cast<void *>(
+ Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventBegin)
+ },
+ {"nativeTraceEventEnd",
+ "()V",
+ reinterpret_cast<void *>(
+ Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeTraceEventEnd)
+ },
+ {"nativeVersion",
+ "()Ljava/lang/String;",
+ reinterpret_cast<void *>(
+ Java_androidx_tracing_perfetto_jni_PerfettoNative_nativeVersion)
+ },
+};
+
+jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
+ JNIEnv *env = NULL;
+ if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6)) {
+ PERFETTO_LOG("JNI_OnLoad failure when trying to register native methods for tracing.");
+ return JNI_ERR;
+ }
+ jclass clazz = env->FindClass("androidx/tracing/perfetto/jni/PerfettoNative");
+ if (clazz == NULL) {
+ PERFETTO_LOG("Cannot find PerfettoNative class when trying to register native methods for "
+ "tracing.");
+ return JNI_ERR;
+ }
+
+ int result = env->RegisterNatives(clazz, sMethods, 4);
+ env->DeleteLocalRef(clazz);
+
+ if (result != 0) {
+ PERFETTO_LOG("Failure when trying to call RegisterNatives to register native methods for "
+ "tracing.");
+ return JNI_ERR;
+ }
+
+ PERFETTO_LOG("Successfully registered native methods for tracing.");
+ return JNI_VERSION_1_6;
+}
\ No newline at end of file
diff --git a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
index 419e348..00e6c5a 100644
--- a/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
+++ b/tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/jni/PerfettoNative.kt
@@ -16,6 +16,8 @@
package androidx.tracing.perfetto.jni
import androidx.tracing.perfetto.security.SafeLibLoader
+import dalvik.annotation.optimization.CriticalNative
+import dalvik.annotation.optimization.FastNative
import java.io.File
internal object PerfettoNative {
@@ -25,10 +27,10 @@
object Metadata {
const val version = "1.0.0-alpha07"
val checksums = mapOf(
- "arm64-v8a" to "abb5fe78f243999cab6fc8ea05e6d0743df3582e7ea4782f5ef2a677d8a0bde7",
- "armeabi-v7a" to "d40c8487f7bca0917dcd6c0626d179394ef4b28d6a2fed809bc34e8ea15a100e",
- "x86" to "38e8e5872e2916b993ca5d37894d7973934730bc427af20de4a52f0656a9ce0f",
- "x86_64" to "ef545e11ceb45bf0eb611d12867bf9b9c5c9361d7e1865f8723a7505d3107ab8",
+ "arm64-v8a" to "3d9fa02a04459e3480f28ae7f3a8ced30f181f02a0e6bc6caf85704faea84857",
+ "armeabi-v7a" to "9eb7bf76a94fce79e682204c9e702c872d15c85f2ebd48de61aa541e7e6de034",
+ "x86" to "6eb72683bf58eeb0175f094d88bc0e00744b1a593dc7eaac9ee15168b1ab9234",
+ "x86_64" to "eabf725464c24c9709d8e2ea073233e5712949bb65aac5c4972528d813b55c74",
)
}
@@ -36,8 +38,17 @@
fun loadLib(file: File, loader: SafeLibLoader) = loader.loadLib(file, Metadata.checksums)
+ @JvmStatic
external fun nativeRegisterWithPerfetto()
+
+ @FastNative
+ @JvmStatic
external fun nativeTraceEventBegin(key: Int, traceInfo: String)
+
+ @CriticalNative
+ @JvmStatic
external fun nativeTraceEventEnd()
+
+ @JvmStatic
external fun nativeVersion(): String
}
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java
new file mode 100644
index 0000000..13fbfd2
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/CriticalNative.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.annotation.optimization;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Applied to native methods to enable an ART runtime built-in optimization:
+ * methods that are annotated this way can speed up JNI transitions for methods that contain no
+ * objects (in parameters or return values, or as an implicit {@code this}).
+ *
+ * <p>
+ * The native implementation must exclude the {@code JNIEnv} and {@code jclass} parameters from its
+ * function signature. As an additional limitation, the method must be explicitly registered with
+ * {@code RegisterNatives} instead of relying on the built-in dynamic JNI linking.
+ * </p>
+ *
+ * <p>
+ * Performance of JNI transitions:
+ * <ul>
+ * <li>Regular JNI cost in nanoseconds: 115
+ * <li>Fast {@code (!)} JNI cost in nanoseconds: 60
+ * <li>{@literal @}{@link FastNative} cost in nanoseconds: 35
+ * <li>{@literal @}{@code CriticalNative} cost in nanoseconds: 25
+ * </ul>
+ * (Measured on angler-userdebug in 07/2016).
+ * </p>
+ *
+ * <p>
+ * A similar annotation, {@literal @}{@link FastNative}, exists with similar performance guarantees.
+ * However, unlike {@code @CriticalNative} it supports non-statics, object return values, and object
+ * parameters. If a method absolutely must have access to a {@code jobject}, then use
+ * {@literal @}{@link FastNative} instead of this.
+ * </p>
+ *
+ * <p>
+ * This has the side-effect of disabling all garbage collections while executing a critical native
+ * method. Use with extreme caution. Any long-running methods must not be marked with
+ * {@code @CriticalNative} (including usually-fast but generally unbounded methods)!
+ * </p>
+ *
+ * <p>
+ * <b>Deadlock Warning:</b> As a rule of thumb, do not acquire any locks during a critical native
+ * call if they aren't also locally released [before returning to managed code].
+ * </p>
+ *
+ * <p>
+ * Say some code does:
+ *
+ * <code>
+ * critical_native_call_to_grab_a_lock();
+ * does_some_java_work();
+ * critical_native_call_to_release_a_lock();
+ * </code>
+ *
+ * <p>
+ * This code can lead to deadlocks. Say thread 1 just finishes
+ * {@code critical_native_call_to_grab_a_lock()} and is in {@code does_some_java_work()}.
+ * GC kicks in and suspends thread 1. Thread 2 now is in
+ * {@code critical_native_call_to_grab_a_lock()} but is blocked on grabbing the
+ * native lock since it's held by thread 1. Now thread suspension can't finish
+ * since thread 2 can't be suspended since it's doing CriticalNative JNI.
+ * </p>
+ *
+ * <p>
+ * Normal natives don't have the issue since once it's executing in native code,
+ * it is considered suspended from the runtime's point of view.
+ * CriticalNative natives however don't do the state transition done by the normal natives.
+ * </p>
+ *
+ * <p>
+ * This annotation has no effect when used with non-native methods.
+ * The runtime must throw a {@code VerifierError} upon class loading if this is used with a native
+ * method that contains object parameters, an object return value, or a non-static.
+ * </p>
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(RetentionPolicy.CLASS) // Save memory, don't instantiate as an object at runtime.
+@Target(ElementType.METHOD)
+public @interface CriticalNative {}
+
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java
new file mode 100644
index 0000000..3eb3745
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/FastNative.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package dalvik.annotation.optimization;
+
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An ART runtime built-in optimization for "native" methods to speed up JNI transitions.
+ *
+ * <p>
+ * This has the side-effect of disabling all garbage collections while executing a fast native
+ * method. Use with extreme caution. Any long-running methods must not be marked with
+ * {@code @FastNative} (including usually-fast but generally unbounded methods)!</p>
+ *
+ * <p><b>Deadlock Warning:</b>As a rule of thumb, do not acquire any locks during a fast native
+ * call if they aren't also locally released [before returning to managed code].</p>
+ *
+ * <p>
+ * Say some code does:
+ *
+ * <code>
+ * fast_jni_call_to_grab_a_lock();
+ * does_some_java_work();
+ * fast_jni_call_to_release_a_lock();
+ * </code>
+ *
+ * <p>
+ * This code can lead to deadlocks. Say thread 1 just finishes
+ * {@code fast_jni_call_to_grab_a_lock()} and is in {@code does_some_java_work()}.
+ * GC kicks in and suspends thread 1. Thread 2 now is in {@code fast_jni_call_to_grab_a_lock()}
+ * but is blocked on grabbing the native lock since it's held by thread 1.
+ * Now thread suspension can't finish since thread 2 can't be suspended since it's doing
+ * FastNative JNI.
+ * </p>
+ *
+ * <p>
+ * Normal JNI doesn't have the issue since once it's in native code,
+ * it is considered suspended from java's point of view.
+ * FastNative JNI however doesn't do the state transition done by JNI.
+ * </p>
+ *
+ * <p>
+ * Note that even in FastNative methods you <b>are</b> allowed to
+ * allocate objects and make upcalls into Java code. A call from Java to
+ * a FastNative function and back to Java is equivalent to a call from one Java
+ * method to another. What's forbidden in a FastNative method is blocking
+ * the calling thread in some non-Java code and thereby preventing the thread
+ * from responding to requests from the garbage collector to enter the suspended
+ * state.
+ * </p>
+ *
+ * <p>
+ * Has no effect when used with non-native methods.
+ * </p>
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@Retention(RetentionPolicy.CLASS) // Save memory, don't instantiate as an object at runtime.
+@Target(ElementType.METHOD)
+public @interface FastNative {}
+
diff --git a/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md
new file mode 100644
index 0000000..e604041
--- /dev/null
+++ b/tracing/tracing-perfetto/src/main/java/dalvik/annotation/optimization/README.md
@@ -0,0 +1,9 @@
+# Rationale
+
+`FastNative` and `CriticalNative` annotations are duplicated from the platform, so that the platform
+detects their presence, and applies them, even though they're not part of platform public API.
+
+# Next steps
+
+Once the annotations become public in the platform, remove duplicate files and use the platform.
+Tracking item: b/35664282
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 796e46e..870a8dc 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.testutils.WithTouchSlop
import androidx.compose.ui.Modifier
@@ -500,6 +501,7 @@
assertThat(state.selectedOption).isNotEqualTo(initialOption)
}
+ @Test
fun scrolls_from_non_canonical_option_works() {
lateinit var scope: CoroutineScope
val pickerDriver = PickerDriver(separationSign = 1)
@@ -534,6 +536,27 @@
pickerDriver.verifyCenterItemIsCentered()
}
+ @Test
+ fun rememberPickerState_updates_after_new_inputs() {
+ val numberOfOptions = 10
+ lateinit var selectedOption: MutableState<Int>
+ rule.setContent {
+ selectedOption = remember { mutableStateOf(1) }
+ val pickerState = rememberPickerState(
+ initialNumberOfOptions = numberOfOptions,
+ initiallySelectedOption = selectedOption.value
+ )
+ Text(text = "${pickerState.selectedOption}")
+ }
+
+ // Update selected option to a new value - should also update the PickerState instance,
+ // and then recompose with the new value in the Text element.
+ selectedOption.value = 2
+ rule.waitForIdle()
+
+ rule.onNodeWithText("2").assertExists()
+ }
+
private fun scroll_snaps(
separationSign: Int = 0,
touchInput: (TouchInjectionScope).() -> Unit,
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
index 8284aad..158cf60 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PlaceholderTest.kt
@@ -21,15 +21,19 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
import org.junit.Rule
import org.junit.Test
import com.google.common.truth.Truth.assertThat
+import kotlin.math.max
class PlaceholderTest {
@get:Rule
@@ -237,17 +241,11 @@
expectedShimmerColor
)
- // Move the start of the next placeholder animation loop and them advance the clock 200
- // milliseconds (PLACEHOLDER_PROGRESSION_DURATION_MS / 4) to show the shimmer.
- //
- // We choose (PLACEHOLDER_PROGRESSION_DURATION_MS / 4) as this should put the center of the
- // shimmer gradiant at the top left (0,0) of the screen and as we have placed the component
- // at the top of the screen this should ensure we have some shimmer gradiant cast over the
- // component regardless of the screen size/shape. So should work for round, square or
- // rectangular screens.
+ // Move the start of the next placeholder animation loop and them advance the clock 267
+ // milliseconds (PLACEHOLDER_PROGRESSION_DURATION_MS / 3) to show the shimmer.
placeholderState.moveToStartOfNextAnimationLoop()
placeholderState.advanceFrameMillisAndCheckState(
- PLACEHOLDER_PROGRESSION_DURATION_MS / 4,
+ PLACEHOLDER_PROGRESSION_DURATION_MS / 3,
PlaceholderStage.ShowPlaceholder
)
@@ -286,6 +284,90 @@
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalWearMaterialApi::class)
@Test
+ fun wipeoff_takes_background_offset_into_account() {
+ var contentReady = false
+ lateinit var placeholderState: PlaceholderState
+ var expectedBackgroundColor = Color.Transparent
+ var expectedBackgroundPlaceholderColor: Color = Color.Transparent
+ rule.setContentWithTheme {
+ placeholderState = rememberPlaceholderState {
+ contentReady
+ }
+ val maxScreenDimensionPx = with(LocalDensity.current) {
+ Dp(max(screenHeightDp(), screenWidthDp()).toFloat()).toPx()
+ }
+ // Set the offset to be 50% of the screen
+ placeholderState.backgroundOffset =
+ Offset(maxScreenDimensionPx / 2f, maxScreenDimensionPx / 2f)
+ expectedBackgroundColor = MaterialTheme.colors.primary
+ expectedBackgroundPlaceholderColor = MaterialTheme.colors.surface
+
+ Chip(
+ modifier = Modifier
+ .testTag("test-item")
+ .fillMaxWidth(),
+ content = {},
+ >
+ colors = PlaceholderDefaults.placeholderChipColors(
+ originalChipColors = ChipDefaults.primaryChipColors(),
+ placeholderState = placeholderState,
+ ),
+ border = ChipDefaults.chipBorder()
+ )
+ }
+
+ placeholderState.initializeTestFrameMillis()
+
+ // Check the background color is correct
+ rule.onNodeWithTag("test-item")
+ .captureToImage()
+ .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+ // Check that there is primary color showing
+ rule.onNodeWithTag("test-item")
+ .captureToImage()
+ .assertDoesNotContainColor(
+ expectedBackgroundColor
+ )
+
+ // Prepare to start to wipe off and show contents.
+ contentReady = true
+
+ placeholderState
+ .advanceToNextPlaceholderAnimationLoopAndCheckStage(PlaceholderStage.WipeOff)
+
+ // Check that placeholder background is still visible
+ rule.onNodeWithTag("test-item")
+ .captureToImage()
+ .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+
+ // Move forward by 25% of the wipe-off and confirm that no wipe-off has happened yet due
+ // to our offset
+ placeholderState.advanceFrameMillisAndCheckState(
+ PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS / 4,
+ PlaceholderStage.WipeOff
+ )
+
+ // Check that placeholder background is still visible
+ rule.onNodeWithTag("test-item")
+ .captureToImage()
+ .assertContainsColor(expectedBackgroundPlaceholderColor, 80f)
+
+ // Now move the end of the wipe-off and confirm that the proper chip background is visible
+ placeholderState.advanceFrameMillisAndCheckState(
+ PLACEHOLDER_PROGRESSION_DURATION_MS -
+ (PLACEHOLDER_PROGRESSION_DURATION_MS / 4),
+ PlaceholderStage.WipeOff
+ )
+
+ // Check that normal chip background is now visible
+ rule.onNodeWithTag("test-item")
+ .captureToImage()
+ .assertContainsColor(expectedBackgroundColor, 80f)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @OptIn(ExperimentalWearMaterialApi::class)
+ @Test
fun placeholder_background_is_correct_color() {
var expectedPlaceholderBackgroundColor = Color.Transparent
var expectedBackgroundColor = Color.Transparent
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
index de17093a..41d50142 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Chip.kt
@@ -1152,7 +1152,7 @@
public fun outlinedChipBorder(
borderColor: Color = MaterialTheme.colors.primaryVariant.copy(alpha = 0.6f),
disabledBorderColor: Color = borderColor.copy(alpha = ContentAlpha.disabled),
- borderWidth: Dp = 2.dp
+ borderWidth: Dp = 1.dp
): ChipBorder {
return DefaultChipBorder(
borderStroke = BorderStroke(borderWidth, borderColor),
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 622ebf0..f2d2d66 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -324,9 +324,14 @@
initialNumberOfOptions: Int,
initiallySelectedOption: Int = 0,
repeatItems: Boolean = true
-): PickerState = rememberSaveable(saver = PickerState.Saver) {
- PickerState(initialNumberOfOptions, initiallySelectedOption, repeatItems)
-}
+): PickerState = rememberSaveable(
+ initialNumberOfOptions,
+ initiallySelectedOption,
+ repeatItems,
+ saver = PickerState.Saver
+ ) {
+ PickerState(initialNumberOfOptions, initiallySelectedOption, repeatItems)
+ }
/**
* A state object that can be hoisted to observe item selection.
@@ -428,7 +433,6 @@
)
},
restore = { saved ->
- @Suppress("UNCHECKED_CAST")
PickerState(
initialNumberOfOptions = saved[0] as Int,
initiallySelectedOption = saved[1] as Int,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
index 5312727..b697c4c 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Placeholder.kt
@@ -15,6 +15,8 @@
*/
package androidx.wear.compose.material
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
import androidx.compose.animation.core.withInfiniteAnimationFrameMillis
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@@ -58,6 +60,23 @@
* that needs to be displayed in a component is not yet available, e.g. it is loading
* asynchronously.
*
+ * A [PlaceholderState] should be created for each component that has placeholder data. The
+ * state is used to coordinate all of the different placeholder effects and animations.
+ *
+ * Placeholder has a number of different effects designed to work together.
+ * [Modifier.placeholder] draws a placeholder shape on top of content that is waiting to load. There
+ * can be multiple placeholders in a component.
+ * [Modifier.placeholderShimmer] does a shimmer animation over the whole component that includes the
+ * placeholders. There should only be one placeholderShimmer for each component.
+ *
+ * Background placeholder effects are used to mask the background of components like chips and cards
+ * until all of the data has loaded. Use [PlaceholderDefaults.placeholderChipColors]
+ * [PlaceholderDefaults.placeholderBackgroundBrush] and
+ * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush] to draw the component
+ * background.
+ *
+ * Once all of the components content is loaded the shimmer will stop and a wipe off animation will
+ * remove the placeholders.
*/
@ExperimentalWearMaterialApi
@Stable
@@ -65,6 +84,21 @@
private val isContentReady: () -> Boolean,
private val maxScreenDimension: Float,
) {
+
+ /**
+ * The offset to apply for any background placeholder animations. This is the global offset of
+ * the component which is having its background painted with
+ * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush],
+ * [PlaceholderDefaults.placeholderBackgroundBrush] or
+ * [PlaceholderDefaults.placeholderChipColors].
+ *
+ * The offset values should be retrieved with [OnGloballyPositionedModifier].
+ *
+ * The offset is used to coordinate placeholder effects such as wipe-off between the difference
+ * placeholder layers.
+ */
+ internal var backgroundOffset: Offset = Offset.Zero
+
/**
* Start the animation of the placeholder state.
*/
@@ -79,13 +113,39 @@
}
/**
- * The current value of the placeholder visual effect gradient progression. The progression
- * is a 45 degree angle sweep across the whole screen running from outside of the Top|Left of
- * the screen to Bottom|Right used as the anchor for shimmer and wipe-off gradient effects.
+ * The current value of the placeholder wipe-off visual effect gradient progression. The
+ * progression is a 45 degree angle sweep across the whole screen running from outside of the
+ * Top|Left of the screen to Bottom|Right used as the anchor for wipe-off gradient effects.
*
* The progression represents the x and y coordinates in pixels of the Top|Left part of the
- * gradient that flows across the screen. The progression will start at -gradientWidth and
- * progress to the maximum screen dimension (max of height/width to create a 45 degree angle).
+ * gradient that flows across the screen. The progression will start at -maxScreenDimension (max
+ * of height/width to create a 45 degree angle) * 1.5f and progress to the
+ * maximumScreenDimension * 1.5f.
+ *
+ * The time taken for this progression to reach the edge of visible screen is
+ * [PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS]
+ */
+ internal val placeholderWipeOffProgression: Float by derivedStateOf {
+ val absoluteProgression =
+ (frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost(
+ PLACEHOLDER_PROGRESSION_DURATION_MS).toFloat() /
+ PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS).coerceAtMost(1f)
+ val progression =
+ lerp(-maxScreenDimension * 1.5f, maxScreenDimension * 1.5f, absoluteProgression)
+ progression
+ }
+
+ /**
+ * The current value of the placeholder visual effect gradient progression. The
+ * progression is a 45 degree angle sweep across the whole screen running from outside of the
+ * Top|Left of the screen to Bottom|Right used as the anchor for wipe-off gradient effects.
+ *
+ * The progression represents the x and y coordinates in pixels of the Top|Left part of the
+ * gradient that flows across the screen. The progression will start at -maxScreenDimension (max
+ * of height/width to create a 45 degree angle) * 1.5f and progress to the
+ * maximumScreenDimension * 1.5f.
+ *
+ * The time taken for this progression is [PLACEHOLDER_PROGRESSION_DURATION_MS]
*/
@ExperimentalWearMaterialApi
public val placeholderProgression: Float by derivedStateOf {
@@ -93,8 +153,9 @@
(frameMillis.value.mod(PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS).coerceAtMost(
PLACEHOLDER_PROGRESSION_DURATION_MS).toFloat() /
PLACEHOLDER_PROGRESSION_DURATION_MS)
- val progression = lerp(-maxScreenDimension, maxScreenDimension, absoluteProgression)
- progression
+ val progression =
+ lerp(-maxScreenDimension * 1.5f, maxScreenDimension * 1.5f, absoluteProgression)
+ progressionInterpolator.transform(progression)
}
/**
@@ -148,14 +209,34 @@
internal val frameMillis = mutableStateOf(0L)
private var startOfNextPlaceholderAnimation = 0L
+
+ private val progressionInterpolator: Easing = CubicBezierEasing(0.3f, 0f, 0.5f, 1f)
}
/**
* Creates a [PlaceholderState] that is remembered across compositions. To start placeholder
* animations run [PlaceholderState.startPlaceholderAnimation].
*
- * @param isContentReady a lambda to determine whether all of the data/content has been loaded
- * and is ready to be displayed.
+ * A [PlaceholderState] should be created for each component that has placeholder data. The
+ * state is used to coordinate all of the different placeholder effects and animations.
+ *
+ * Placeholder has a number of different effects designed to work together.
+ * [Modifier.placeholder] draws a placeholder shape on top of content that is waiting to load. There
+ * can be multiple placeholders in a component.
+ * [Modifier.placeholderShimmer] does a shimmer animation over the whole component that includes the
+ * placeholders. There should only be one placeholderShimmer for each component.
+ *
+ * Background placeholder effects are used to mask the background of components like chips and cards
+ * until all of the data has loaded. Use [PlaceholderDefaults.placeholderChipColors]
+ * [PlaceholderDefaults.placeholderBackgroundBrush] and
+ * [PlaceholderDefaults.painterWithPlaceholderOverlayBackgroundBrush] to draw the component
+ * background.
+ *
+ * Once all of the components content is loaded, [isContentReady] is `true` the shimmer will stop
+ * and a wipe off animation will remove the placeholders to reveal the content.
+ *
+ * @param isContentReady a lambda to determine whether all of the data/content has been loaded for a
+ * given component and is ready to be displayed.
*/
@ExperimentalWearMaterialApi
@Composable
@@ -416,7 +497,7 @@
return PlaceholderBackgroundPainter(
painter = null,
placeholderState = placeholderState,
- color = color
+ color = color,
)
}
}
@@ -470,12 +551,14 @@
color
),
start = Offset(
- x = placeholderState.placeholderProgression - offset.x,
- y = placeholderState.placeholderProgression - offset.y
+ x = placeholderState.placeholderWipeOffProgression - offset.x,
+ y = placeholderState.placeholderWipeOffProgression - offset.y
),
end = Offset(
- x = placeholderState.placeholderProgression - offset.x + placeholderState.gradientWidth,
- y = placeholderState.placeholderProgression - offset.y + placeholderState.gradientWidth
+ x = placeholderState.placeholderWipeOffProgression -
+ offset.x + placeholderState.gradientWidth,
+ y = placeholderState.placeholderWipeOffProgression -
+ offset.y + placeholderState.gradientWidth
)
)
}
@@ -492,10 +575,6 @@
private var alpha: Float = 1.0f
) : Painter() {
override fun DrawScope.onDraw() {
- val offset = Offset(
- this.center.x - (this.size.width / 2f),
- this.center.y - (this.size.height / 2f)
- )
// Due to anti aliasing we can not use a SolidColor brush over the top of the background
// painter without seeing some background color bleeding through. As a result we use
// the colorFilter to tint the normal background painter instead - b/253667329
@@ -503,7 +582,7 @@
PlaceholderStage.WipeOff -> {
wipeOffBrush(
color,
- offset,
+ placeholderState.backgroundOffset,
placeholderState
) to null
}
@@ -667,6 +746,10 @@
alpha: Float = 1.0f,
val shape: Shape
) : AbstractPlaceholderModifier(alpha, shape) {
+ override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
+ placeholderState.backgroundOffset = coordinates.positionInRoot()
+ super.onGloballyPositioned(coordinates)
+ }
override fun generateBrush(offset: Offset): Brush? {
return if (placeholderState.placeholderStage == PlaceholderStage.ShowPlaceholder) {
Brush.linearGradient(
@@ -711,6 +794,7 @@
}
internal const val PLACEHOLDER_PROGRESSION_DURATION_MS = 800L
+internal const val PLACEHOLDER_WIPE_OFF_PROGRESSION_DURATION_MS = 250L
internal const val PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS = 800L
internal const val PLACEHOLDER_GAP_BETWEEN_ANIMATION_LOOPS_MS =
PLACEHOLDER_PROGRESSION_DURATION_MS + PLACEHOLDER_DELAY_BETWEEN_PROGRESSIONS_MS
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index d4f0e89..87deb2d 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -61,12 +61,9 @@
scrollState: LazyListState,
itemIndex: Int = 0,
offset: Dp = 0.dp,
-): Modifier {
- val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
- return if (targetItem != null) {
- scrollAway { -targetItem.offset - offset.toPx() }
- } else {
- ignore()
+): Modifier = scrollAway {
+ scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+ -it.offset - offset.toPx()
}
}
@@ -84,55 +81,46 @@
scrollState: ScalingLazyListState,
itemIndex: Int = 1,
offset: Dp = 0.dp,
-): Modifier {
- val targetItem = scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
- return if (targetItem != null) {
- scrollAway { -targetItem.offset - offset.toPx() }
- } else {
- ignore()
+): Modifier = scrollAway {
+ scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+ -it.offset - offset.toPx()
}
}
-private fun Modifier.scrollAway(yPxFn: Density.() -> Float): Modifier = this.then(
- object : LayoutModifier {
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val placeable = measurable.measure(constraints)
- return layout(placeable.width, placeable.height) {
+private fun Modifier.scrollAway(yPxFn: Density.() -> Float?): Modifier =
+ this.then(
+ object : LayoutModifier {
+ override fun MeasureScope.measure(
+ measurable: Measurable,
+ constraints: Constraints
+ ): MeasureResult {
val yPx = yPxFn()
- val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
- val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
- val offsetY = -(maxOffset.toPx() * progress).toInt()
+ if (yPx == null) {
+ return object : MeasureResult {
+ override val width = 0
+ override val height = 0
+ override val alignmentLines = mapOf<AlignmentLine, Int>()
+ override fun placeChildren() {}
+ }
+ } else {
+ val placeable = measurable.measure(constraints)
+ return layout(placeable.width, placeable.height) {
+ val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
+ val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
+ val offsetY = -(maxOffset.toPx() * progress).toInt()
- placeable.placeWithLayer(0, offsetY) {
- alpha = motionFraction
- scaleX = motionFraction
- scaleY = motionFraction
- transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+ placeable.placeWithLayer(0, offsetY) {
+ alpha = motionFraction
+ scaleX = motionFraction
+ scaleY = motionFraction
+ transformOrigin =
+ TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+ }
+ }
}
}
}
- }
-)
-
-// Trivial modifier that neither measures nor places the content.
-private fun Modifier.ignore(): Modifier = this.then(
- object : LayoutModifier {
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- return object : MeasureResult {
- override val width = 0
- override val height = 0
- override val alignmentLines = mapOf<AlignmentLine, Int>()
- override fun placeChildren() {}
- }
- }
- }
-)
+ )
// The scroll motion effects take place between 0dp and 36dp.
internal val maxScrollOut = 36.dp
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
index 05ff127..7264351 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/SwipeToDismissBox.kt
@@ -16,15 +16,15 @@
package androidx.wear.compose.material
-import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.SpringSpec
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.TweenSpec
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -38,11 +38,10 @@
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
-import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -53,12 +52,12 @@
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
-import kotlin.math.PI
+import androidx.compose.ui.util.lerp
+import kotlin.math.max
+import kotlin.math.min
import kotlin.math.roundToInt
-import kotlin.math.sin
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
@@ -101,7 +100,7 @@
state: SwipeToDismissBoxState,
modifier: Modifier = Modifier,
backgroundScrimColor: Color = MaterialTheme.colors.background,
- contentScrimColor: Color = contentColorFor(backgroundScrimColor),
+ contentScrimColor: Color = MaterialTheme.colors.background,
backgroundKey: Any = SwipeToDismissKeys.Background,
contentKey: Any = SwipeToDismissKeys.Content,
hasBackground: Boolean = true,
@@ -118,61 +117,78 @@
state = state.swipeableState,
enabled = hasBackground,
anchors = anchors(maxWidth),
- thresholds = { _, _ -> FractionalThreshold(SwipeThreshold) },
+ thresholds = { _, _ -> FractionalThreshold(SWIPE_THRESHOLD) },
resistance = ResistanceConfig(
basis = maxWidth,
- factorAtMin = TotalResistance,
- factorAtMax = TotalResistance,
+ factorAtMin = TOTAL_RESISTANCE,
+ factorAtMax = TOTAL_RESISTANCE,
),
orientation = Orientation.Horizontal,
)
) {
- val dismissAnimatable = remember { Animatable(0f) }
+ // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
+
+ var squeezeMode by remember {
+ mutableStateOf(true)
+ }
LaunchedEffect(state.isAnimationRunning) {
if (state.targetValue == SwipeToDismissValue.Dismissed) {
- dismissAnimatable.animateTo(1f, SpringSpec())
- } else {
- // because SwipeToDismiss remains alive, it worth resetting animation to 0
- // when [targetValue] becomes [Original] again
- dismissAnimatable.snapTo(0f)
+ squeezeMode = false
}
}
- // Use remember { derivedStateOf{ ... } } idiom to re-use modifiers where possible.
+ LaunchedEffect(state.targetValue) {
+ if (!squeezeMode && state.targetValue == SwipeToDismissValue.Default) {
+ squeezeMode = true
+ }
+ }
+
val isRound = isRoundDevice()
val modifiers by remember(isRound, backgroundScrimColor) {
derivedStateOf {
- val squeezeMotion =
- SqueezeMotion(state.swipeableState.offset.value.roundToInt(), maxWidth)
+ val progress = (state.swipeableState.offset.value / maxWidth).coerceIn(0f, 1f)
+ val scale = lerp(SCALE_MAX, SCALE_MIN, progress).coerceIn(SCALE_MIN, SCALE_MAX)
+ val squeezeOffset = max(0f, (1f - scale) * maxWidth / 2f)
+ val slideOffset = lerp(squeezeOffset, maxWidth, max(0f, progress - 0.7f) / 0.3f)
+
+ val translationX = if (squeezeMode) squeezeOffset else slideOffset
+
+ val backgroundAlpha =
+ lerp(
+ MAX_BACKGROUND_SCRIM_ALPHA,
+ MIN_BACKGROUND_SCRIM_ALPHA,
+ BACKGROUND_SCRIM_EASING.transform(progress)
+ )
+ val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
Modifiers(
- contentForeground =
- Modifier.offset { IntOffset(squeezeMotion.contentOffset, 0) }
+ contentForeground = Modifier
.fillMaxSize()
- .scale(squeezeMotion.scale(dismissAnimatable.value))
+ .graphicsLayer(
+ translationX = translationX,
+ scaleX = scale,
+ scaleY = scale,
+ )
.then(
- if (isRound && squeezeMotion.contentOffset > 0) {
+ if (isRound && translationX > 0) {
Modifier.clip(CircleShape)
} else {
Modifier
}
)
- .alpha(1 - dismissAnimatable.value)
.background(backgroundScrimColor),
scrimForeground =
- Modifier.background(
- contentScrimColor.copy(alpha = squeezeMotion.contentScrimAlpha)
- ).fillMaxSize(),
- scrimBackground =
- Modifier.matchParentSize()
+ Modifier
.background(
- backgroundScrimColor
- .copy(
- alpha = squeezeMotion.backgroundScrimAlpha(
- dismissAnimatable.value
- )
- )
+ contentScrimColor.copy(alpha = contentScrimAlpha)
+ )
+ .fillMaxSize(),
+ scrimBackground =
+ Modifier
+ .matchParentSize()
+ .background(
+ backgroundScrimColor.copy(alpha = backgroundAlpha)
)
)
}
@@ -313,9 +329,9 @@
get() = swipeableState.isAnimationRunning
internal fun edgeNestedScrollConnection(
- swipeState: State<SwipeState>
+ edgeSwipeState: State<EdgeSwipeState>
): NestedScrollConnection =
- swipeableState.edgeNestedScrollConnection(swipeState)
+ swipeableState.edgeNestedScrollConnection(edgeSwipeState)
/**
* Set the state without any animation and suspend until it's set
@@ -326,14 +342,14 @@
private companion object {
private fun <T> SwipeableState<T>.edgeNestedScrollConnection(
- swipeState: State<SwipeState>
+ edgeSwipeState: State<EdgeSwipeState>
): NestedScrollConnection =
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.x
// If swipeState = SwipeState.SWIPING_TO_DISMISS - perform swipeToDismiss
// drag and consume everything
- return if (swipeState.value == SwipeState.SwipingToDismiss &&
+ return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss &&
source == NestedScrollSource.Drag
) {
performDrag(delta)
@@ -352,8 +368,8 @@
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.x
// Consumes fling by SwipeToDismiss
- return if (swipeState.value == SwipeState.SwipingToDismiss ||
- swipeState.value == SwipeState.SwipeToDismissInProgress
+ return if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss ||
+ edgeSwipeState.value == EdgeSwipeState.SwipeToDismissInProgress
) {
performFling(velocity = toFling)
available
@@ -386,7 +402,7 @@
*/
@Composable
public fun rememberSwipeToDismissBoxState(
- animationSpec: AnimationSpec<Float> = SwipeToDismissBoxDefaults.AnimationSpec,
+ animationSpec: AnimationSpec<Float> = SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC,
confirmStateChange: (SwipeToDismissValue) -> Boolean = { true },
): SwipeToDismissBoxState {
return remember(animationSpec, confirmStateChange) {
@@ -474,10 +490,10 @@
}
) {
// Tracks the current swipe status
- val swipeState = remember { mutableStateOf(SwipeState.WaitingForTouch) }
+ val edgeSwipeState = remember { mutableStateOf(EdgeSwipeState.WaitingForTouch) }
val nestedScrollConnection =
remember(swipeToDismissBoxState) {
- swipeToDismissBoxState.edgeNestedScrollConnection(swipeState)
+ swipeToDismissBoxState.edgeNestedScrollConnection(edgeSwipeState)
}
val nestedPointerInput: suspend PointerInputScope.() -> Unit = {
@@ -492,21 +508,21 @@
// the next touch. If it lands to the left of the first, we consider
// it as a swipe left and set the state to SwipingToPage. Otherwise,
// set the state to SwipingToDismiss
- when (swipeState.value) {
- SwipeState.SwipeToDismissInProgress,
- SwipeState.WaitingForTouch -> {
- swipeState.value =
+ when (edgeSwipeState.value) {
+ EdgeSwipeState.SwipeToDismissInProgress,
+ EdgeSwipeState.WaitingForTouch -> {
+ edgeSwipeState.value =
if (change.position.x < edgeWidth.toPx())
- SwipeState.EdgeClickedWaitingForDirection
+ EdgeSwipeState.EdgeClickedWaitingForDirection
else
- SwipeState.SwipingToPage
+ EdgeSwipeState.SwipingToPage
}
- SwipeState.EdgeClickedWaitingForDirection -> {
- swipeState.value =
+ EdgeSwipeState.EdgeClickedWaitingForDirection -> {
+ edgeSwipeState.value =
if (change.position.x < change.previousPosition.x)
- SwipeState.SwipingToPage
+ EdgeSwipeState.SwipingToPage
else
- SwipeState.SwipingToDismiss
+ EdgeSwipeState.SwipingToDismiss
}
else -> {} // Do nothing
}
@@ -514,11 +530,11 @@
// or to SwipeToDismissInProgress if current
// state is SwipingToDismiss
if (change.changedToUp()) {
- swipeState.value =
- if (swipeState.value == SwipeState.SwipingToDismiss)
- SwipeState.SwipeToDismissInProgress
+ edgeSwipeState.value =
+ if (edgeSwipeState.value == EdgeSwipeState.SwipingToDismiss)
+ EdgeSwipeState.SwipeToDismissInProgress
else
- SwipeState.WaitingForTouch
+ EdgeSwipeState.WaitingForTouch
}
}
}
@@ -532,7 +548,7 @@
/**
* An enum which represents a current state of swipe action.
*/
-internal enum class SwipeState {
+internal enum class EdgeSwipeState {
// Waiting for touch, edge was not touched before.
WaitingForTouch,
@@ -551,70 +567,6 @@
}
/**
- * A class which is responsible for squeezing animation and all computations related to it
- */
-private class SqueezeMotion(
- private val offsetPx: Int,
- private val maxWidth: Float
-) {
- private val scaleDelta = 0.2f
- private val dismissScaleDelta = 0.05f
- private val offsetFactor = scaleDelta / 2
- private val contentScrimMaxAlpha = 0.07f
- private val backgroundScrimMinAlpha = 0.65f
-
- private val progress = calculateProgress(offsetPx.toFloat(), maxWidth)
-
- /**
- * [scale] can change from 1 to 1-[scaleDelta] - [dismissScaleDelta]
- * As [progress] goes from 0 to 1 and [finalAnimationProgress] from 0 to 1,
- * [scale] decreases accordingly up to a 1 - [scaleDelta] - [dismissScaleDelta]
- */
- fun scale(finalAnimationProgress: Float): Float =
- (1.0 - progress * scaleDelta - finalAnimationProgress * dismissScaleDelta).toFloat()
-
- /**
- * As [progress] goes from 0 to 1, [contentOffset] changes from 0 to maxWidth
- * mutiplied by [offsetFactor].
- * If [offsetPx] negative ( <= 0) it remains the same.
- * This helps to have a resistance animation if page is swiped to the opposite
- * direction.
- */
- val contentOffset: Int
- get() = if (offsetPx > 0)
- (maxWidth * progress * offsetFactor).toInt()
- else
- offsetPx
-
- /**
- * As [progress] goes from 0 to 1, [contentScrimAlpha] changes from 0%
- * to [contentScrimMaxAlpha].
- */
- val contentScrimAlpha: Float
- get() = contentScrimMaxAlpha * progress
-
- /**
- * As [progress] goes from 0 to 1, [backgroundScrimAlpha] is decreasing from 100% to
- * [backgroundScrimMinAlpha] value. [finalAnimationProgress] makes background completely
- * transparent by changing its value from [backgroundScrimMinAlpha] to 0.
- */
- fun backgroundScrimAlpha(finalAnimationProgress: Float): Float =
- 1 - (1 - backgroundScrimMinAlpha) * progress -
- backgroundScrimMinAlpha * finalAnimationProgress
-
- /**
- * Computes a progress, which is in range from 0 to 1. As offset value changes from 0 to basis,
- * the progress changes by sin function
- */
- private fun calculateProgress(
- offset: Float,
- basis: Float
- ): Float = if (offset > 0)
- sin((offset / basis).coerceIn(-1f, 1f) * PI.toFloat() / 2)
- else 0f
-}
-
-/**
* Class to enable calculating group of modifiers in a single, memoised block.
*/
private data class Modifiers(
@@ -630,5 +582,13 @@
maxWidth to SwipeToDismissValue.Dismissed
)
-private val SwipeThreshold = 0.5f
-private val TotalResistance = 1000f
\ No newline at end of file
+private const val SWIPE_THRESHOLD = 0.5f
+private const val TOTAL_RESISTANCE = 1000f
+private const val SCALE_MAX = 1f
+private const val SCALE_MIN = 0.7f
+private const val MAX_CONTENT_SCRIM_ALPHA = 0.3f
+private const val MAX_BACKGROUND_SCRIM_ALPHA = 0.75f
+private const val MIN_BACKGROUND_SCRIM_ALPHA = 0f
+private val BACKGROUND_SCRIM_EASING = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
+private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
+ TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
index 145e8b0..b87eb398 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PlaceholderDemo.kt
@@ -371,7 +371,8 @@
Box {
Chip(
- modifier = modifier.fillMaxWidth(),
+ modifier = modifier
+ .fillMaxWidth(),
>
label = {
Text(
@@ -463,7 +464,7 @@
},
enabled = true,
colors = PlaceholderDefaults.placeholderChipColors(
- placeholderState = chipPlaceholderState,
+ placeholderState = chipPlaceholderState
)
)
}
@@ -609,7 +610,7 @@
MaterialTheme.shapes.large
),
backgroundPainter = PlaceholderDefaults.placeholderBackgroundBrush(
- placeholderState = cardPlaceholderState,
+ placeholderState = cardPlaceholderState
)
) {
Spacer(modifier = Modifier.height(4.dp))
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index c30e3fa..26ca147 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -42,15 +42,6 @@
import androidx.test.screenshot.assertAgainstGolden
import androidx.wear.watchface.BoundingArc
import androidx.wear.watchface.CanvasComplication
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationText
-import androidx.wear.watchface.complications.data.ComplicationType
-import androidx.wear.watchface.complications.data.LongTextComplicationData
-import androidx.wear.watchface.complications.data.PlainComplicationText
-import androidx.wear.watchface.complications.data.RangedValueComplicationData
-import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotBoundsType
@@ -73,24 +64,35 @@
import androidx.wear.watchface.client.WatchFaceClientExperimental
import androidx.wear.watchface.client.WatchFaceControlClient
import androidx.wear.watchface.client.WatchUiState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationExperimental
+import androidx.wear.watchface.complications.data.ComplicationText
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.LongTextComplicationData
import androidx.wear.watchface.complications.data.NoDataComplicationData
+import androidx.wear.watchface.complications.data.PlainComplicationText
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
import androidx.wear.watchface.control.IInteractiveWatchFace
import androidx.wear.watchface.control.WatchFaceControlService
-import androidx.wear.watchface.samples.BLUE_STYLE
-import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
-import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING
-import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE_PKG
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.DRAW_HOUR_PIPS_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.LEFT_COMPLICATION
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.WATCH_HAND_LENGTH_STYLE_SETTING
import androidx.wear.watchface.samples.ExampleOpenGLBackgroundInitWatchFaceService
-import androidx.wear.watchface.samples.GREEN_STYLE
-import androidx.wear.watchface.samples.LEFT_COMPLICATION
-import androidx.wear.watchface.samples.NO_COMPLICATIONS
import androidx.wear.watchface.samples.R
-import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleData
@@ -100,6 +102,12 @@
import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
import androidx.wear.watchface.style.WatchFaceLayer
import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
@@ -119,12 +127,6 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
-import java.time.Instant
-import java.time.ZoneId
-import java.time.ZonedDateTime
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.TimeoutException
private const val CONNECT_TIMEOUT_MILLIS = 500L
private const val DESTROY_TIMEOUT_MILLIS = 500L
@@ -359,7 +361,7 @@
headlessInstance.close()
}
- @Suppress("DEPRECATION") // defaultDataSourceType
+ @Suppress("DEPRECATION", "NewApi") // defaultDataSourceType
@Test
fun headlessComplicationDetails() {
val headlessInstance = service.createHeadlessWatchFaceClient(
@@ -388,6 +390,8 @@
)
assertThat(leftComplicationDetails.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -411,6 +415,8 @@
)
assertThat(rightComplicationDetails.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -601,8 +607,11 @@
assertThat(flavorA.style.userStyleMap.containsKey("color_style_setting"))
assertThat(flavorA.style.userStyleMap.containsKey("watch_hand_length_style_setting"))
assertThat(flavorA.complications.containsKey(EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID))
- assertThat(flavorA.complications.containsKey(
- EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID))
+ assertThat(
+ flavorA.complications.containsKey(
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+ )
+ )
headlessInstance.close()
}
@@ -704,7 +713,7 @@
}
}
- @Suppress("DEPRECATION") // defaultDataSourceType
+ @Suppress("DEPRECATION", "newApi") // defaultDataSourceType & ComplicationType
@Test
fun interactiveWatchFaceClient_ComplicationDetails() {
val deferredInteractiveInstance = handlerCoroutineScope.async {
@@ -740,6 +749,8 @@
)
assertThat(leftComplicationDetails.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -769,6 +780,8 @@
)
assertThat(rightComplicationDetails.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -1080,23 +1093,23 @@
)
(wallpaperService as TestExampleCanvasAnalogWatchFaceService)
.watchFace.renderer.additionalContentDescriptionLabels = listOf(
- Pair(
- 0,
- ContentDescriptionLabel(
- PlainComplicationText.Builder("Before").build(),
- Rect(10, 10, 20, 20),
- pendingIntent1
- )
- ),
- Pair(
- 20000,
- ContentDescriptionLabel(
- PlainComplicationText.Builder("After").build(),
- Rect(30, 30, 40, 40),
- pendingIntent2
- )
+ Pair(
+ 0,
+ ContentDescriptionLabel(
+ PlainComplicationText.Builder("Before").build(),
+ Rect(10, 10, 20, 20),
+ pendingIntent1
+ )
+ ),
+ Pair(
+ 20000,
+ ContentDescriptionLabel(
+ PlainComplicationText.Builder("After").build(),
+ Rect(30, 30, 40, 40),
+ pendingIntent2
)
)
+ )
val sysUiInterface =
service.getInteractiveWatchFaceClientInstance("testId")!!
@@ -1273,8 +1286,8 @@
androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType(
DefaultComplicationDataSourcePolicy(
ComponentName(
- androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG,
- androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
+ CONFIGURABLE_DATA_SOURCE_PKG,
+ CONFIGURABLE_DATA_SOURCE
),
ComplicationType.SHORT_TEXT,
SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
@@ -1308,8 +1321,8 @@
androidx.wear.watchface.client.DefaultComplicationDataSourcePolicyAndType(
DefaultComplicationDataSourcePolicy(
ComponentName(
- androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG,
- androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
+ CONFIGURABLE_DATA_SOURCE_PKG,
+ CONFIGURABLE_DATA_SOURCE
),
ComplicationType.SHORT_TEXT,
SystemDataSources.DATA_SOURCE_DAY_OF_WEEK,
@@ -2448,7 +2461,8 @@
canvas: Canvas,
bounds: Rect,
zonedDateTime: ZonedDateTime
- ) {}
+ ) {
+ }
}
return WatchFace(WatchFaceType.DIGITAL, renderer)
}
@@ -2495,7 +2509,7 @@
enabled = true,
nameResourceId = R.string.left_complication_screen_name,
screenReaderNameResourceId =
- R.string.left_complication_screen_reader_name
+ R.string.left_complication_screen_reader_name
)
)
)
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
index b65515b..7a59c11 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
@@ -27,16 +27,16 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.wear.watchface.BoundingArc
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
-import androidx.wear.watchface.client.WatchFaceMetadataClient
-import androidx.wear.watchface.control.WatchFaceControlService
import androidx.wear.watchface.ComplicationSlotBoundsType
+import androidx.wear.watchface.client.WatchFaceMetadataClient
+import androidx.wear.watchface.complications.SystemDataSources
import androidx.wear.watchface.complications.data.ComplicationExperimental
-import androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE
-import androidx.wear.watchface.samples.CONFIGURABLE_DATA_SOURCE_PKG
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.control.WatchFaceControlService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.CONFIGURABLE_DATA_SOURCE_PKG
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
import androidx.wear.watchface.style.UserStyle
import androidx.wear.watchface.style.UserStyleSetting
import com.google.common.truth.Truth
@@ -134,6 +134,7 @@
}
@Test
+ @Suppress("NewApi")
public fun getComplicationSlotMetadataMap() {
val service = createWatchFaceMetadataClient(exampleWatchFaceComponentName)
val complicationSlotMetadataMap = service.getComplicationSlotMetadataMap()
@@ -156,6 +157,8 @@
).isEqualTo(ComplicationType.SHORT_TEXT)
Truth.assertThat(leftComplicationMetadata.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -181,6 +184,8 @@
).isEqualTo(ComplicationType.SHORT_TEXT)
Truth.assertThat(rightComplicationMetadata.supportedTypes).containsExactly(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -365,7 +370,8 @@
// check settings' defaults
val userStyleMap = flavor.style.userStyleMap
Truth.assertThat(userStyleMap.keys).containsExactly(
- "color_style_setting", "watch_hand_length_style_setting")
+ "color_style_setting", "watch_hand_length_style_setting"
+ )
val userStyle = UserStyle(flavor.style, schema)
Truth.assertThat(userStyle[UserStyleSetting.Id("color_style_setting")]!!.id)
@@ -376,7 +382,8 @@
Truth.assertThat(complications.keys).containsExactly(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
- EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID)
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+ )
val left = complications[EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID]!!
val right = complications[EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID]!!
@@ -384,19 +391,27 @@
Truth.assertThat(left.primaryDataSource).isNull()
Truth.assertThat(left.secondaryDataSource).isNull()
Truth.assertThat(left.systemDataSourceFallback).isEqualTo(
- SystemDataSources.DATA_SOURCE_DAY_OF_WEEK)
+ SystemDataSources.DATA_SOURCE_DAY_OF_WEEK
+ )
Truth.assertThat(left.systemDataSourceFallbackDefaultType).isEqualTo(
- ComplicationType.SHORT_TEXT)
+ ComplicationType.SHORT_TEXT
+ )
- Truth.assertThat(right.primaryDataSource).isEqualTo(ComponentName(
- CONFIGURABLE_DATA_SOURCE_PKG, CONFIGURABLE_DATA_SOURCE))
+ Truth.assertThat(right.primaryDataSource).isEqualTo(
+ ComponentName(
+ CONFIGURABLE_DATA_SOURCE_PKG, CONFIGURABLE_DATA_SOURCE
+ )
+ )
Truth.assertThat(right.primaryDataSourceDefaultType).isEqualTo(
- ComplicationType.SHORT_TEXT)
+ ComplicationType.SHORT_TEXT
+ )
Truth.assertThat(right.secondaryDataSource).isNull()
Truth.assertThat(right.systemDataSourceFallback).isEqualTo(
- SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET)
+ SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET
+ )
Truth.assertThat(right.systemDataSourceFallbackDefaultType).isEqualTo(
- ComplicationType.SHORT_TEXT)
+ ComplicationType.SHORT_TEXT
+ )
}
@Test
@@ -405,20 +420,24 @@
WatchFaceMetadataClient.isXmlVersionCompatible(
context,
context.resources,
- context.packageName)).isTrue()
+ context.packageName
+ )
+ ).isTrue()
Truth.assertThat(
WatchFaceMetadataClient.isXmlVersionCompatible(
context,
context.resources,
context.packageName,
OutdatedWatchFaceControlTestService::class.java.name
- )).isFalse()
+ )
+ ).isFalse()
Truth.assertThat(
WatchFaceMetadataClient.isXmlVersionCompatible(
context,
context.resources,
"non.existing.package",
"non.existing.package.Service"
- )).isFalse()
+ )
+ ).isFalse()
}
}
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml b/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
index 6215d9d..49a47bc 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/AndroidManifest.xml
@@ -100,6 +100,82 @@
</intent-filter>
</service>
+ <service
+ android:name=
+ "androidx.wear.watchface.complications.datasource.samples.ColorRampDataSourceService"
+ android:label="@string/color_ramp_data_source_name"
+ android:exported="true"
+ android:icon="@drawable/circle"
+ android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+ <meta-data
+ android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ android:value="RANGED_VALUE"/>
+ <meta-data
+ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ android:value="0"/>
+ <intent-filter>
+ <action android:name=
+ "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=
+ "androidx.wear.watchface.complications.datasource.samples.NonInterpolatedColorRampDataSourceService"
+ android:label="@string/color_ramp2_data_source_name"
+ android:exported="true"
+ android:icon="@drawable/circle"
+ android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+ <meta-data
+ android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ android:value="RANGED_VALUE"/>
+ <meta-data
+ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ android:value="0"/>
+ <intent-filter>
+ <action android:name=
+ "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=
+ "androidx.wear.watchface.complications.datasource.samples.GoalProgressDataSourceService"
+ android:label="@string/goal_progress_data_source_name"
+ android:exported="true"
+ android:icon="@drawable/circle"
+ android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+ <meta-data
+ android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ android:value="GOAL_PROGRESS"/>
+ <meta-data
+ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ android:value="0"/>
+ <intent-filter>
+ <action android:name=
+ "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+ </intent-filter>
+ </service>
+
+ <service
+ android:name=
+ "androidx.wear.watchface.complications.datasource.samples.WeightedElementDataSourceService"
+ android:label="@string/weighted_elements_data_source_name"
+ android:exported="true"
+ android:icon="@drawable/circle"
+ android:permission="com.google.android.wearable.permission.BIND_COMPLICATION_PROVIDER">
+ <meta-data
+ android:name="android.support.wearable.complications.SUPPORTED_TYPES"
+ android:value="WEIGHTED_ELEMENTS"/>
+ <meta-data
+ android:name="android.support.wearable.complications.UPDATE_PERIOD_SECONDS"
+ android:value="0"/>
+ <intent-filter>
+ <action android:name=
+ "android.support.wearable.complications.ACTION_COMPLICATION_UPDATE_REQUEST"/>
+ </intent-filter>
+ </service>
+
<service android:name=".ConfigurableDataSourceService"
android:label="@string/configurable_data_source_name"
android:exported="true"
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
index 33d4247..d718dbb 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/AsynchronousDataSourceService.kt
@@ -20,6 +20,7 @@
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
+import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.data.ComplicationText
@@ -67,7 +68,7 @@
}
}
- override fun getPreviewData(type: ComplicationType) = when (type) {
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
ComplicationType.SHORT_TEXT ->
ShortTextComplicationData.Builder(
plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
index 7fa3f3e..29d15de 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/BackgroundDataSourceService.kt
@@ -19,6 +19,7 @@
import android.content.ComponentName
import android.os.Handler
import android.os.Looper
+import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceUpdateRequester
@@ -80,7 +81,7 @@
)
}
- override fun getPreviewData(type: ComplicationType) = when (type) {
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
ComplicationType.SHORT_TEXT ->
ShortTextComplicationData.Builder(
plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt
new file mode 100644
index 0000000..9b82b9a
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ColorRampDataSourceService.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [RangedValueComplicationData] with an interpolated color ramp. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class ColorRampDataSourceService : ComplicationDataSourceService() {
+
+ override fun onComplicationRequest(
+ request: ComplicationRequest,
+ listener: ComplicationRequestListener
+ ) {
+ listener.onComplicationData(
+ RangedValueComplicationData.Builder(
+ value = 75f,
+ min = 0.0f,
+ max = 100.0f,
+ plainText("Many colors")
+ ).setText(plainText("Colors"))
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(
+ Color.GREEN,
+ Color.YELLOW,
+ Color.argb(255, 255, 255, 0),
+ Color.RED,
+ Color.argb(255, 255, 0, 255),
+ Color.argb(255, 92, 64, 51)
+ ),
+ interpolated = true
+ )
+ )
+ .build()
+ )
+ }
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+ ComplicationType.RANGED_VALUE ->
+ RangedValueComplicationData.Builder(
+ value = 10f,
+ min = 0.0f,
+ max = 100.0f,
+ plainText("Many colors")
+ ).setText(plainText("Colors"))
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(
+ Color.GREEN,
+ Color.YELLOW,
+ Color.argb(255, 255, 255, 0),
+ Color.RED,
+ Color.argb(255, 255, 0, 255),
+ Color.argb(255, 92, 64, 51)
+ ),
+ interpolated = true
+ )
+ )
+ .build()
+
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
index e9df7f6..6ab1aca 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ConfigurableDataSourceService.kt
@@ -38,11 +38,9 @@
listener.onComplicationData(makeComplicationData(value))
}
- override fun getPreviewData(type: ComplicationType): ComplicationData? {
- return when (type) {
- ComplicationType.SHORT_TEXT -> makeComplicationData(DEFAULT_VALUE)
- else -> null
- }
+ override fun getPreviewData(type: ComplicationType) = when (type) {
+ ComplicationType.SHORT_TEXT -> makeComplicationData(DEFAULT_VALUE)
+ else -> null
}
private fun makeComplicationData(value: Int): ComplicationData {
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt
new file mode 100644
index 0000000..3108039
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/GoalProgressDataSourceService.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.GoalProgressComplicationData
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [GoalProgressComplicationData]. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class GoalProgressDataSourceService : ComplicationDataSourceService() {
+
+ override fun onComplicationRequest(
+ request: ComplicationRequest,
+ listener: ComplicationRequestListener
+ ) {
+ listener.onComplicationData(
+ GoalProgressComplicationData.Builder(
+ value = 12345.0f,
+ targetValue = 10000.0f,
+ plainText("12345 steps")
+ ).setText(plainText("12345"))
+ .setTitle(plainText("Steps"))
+ .build()
+ )
+ }
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+ ComplicationType.GOAL_PROGRESS ->
+ GoalProgressComplicationData.Builder(
+ value = 1024.0f,
+ targetValue = 10000.0f,
+ plainText("Steps complication")
+ ).setText(plainText("1024"))
+ .setTitle(plainText("Steps"))
+ .build()
+
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
index a2d7957..0481ad4 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/ImmediateDataSourceService.kt
@@ -16,6 +16,7 @@
package androidx.wear.watchface.complications.datasource.samples
+import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
import androidx.wear.watchface.complications.datasource.ComplicationRequest
import androidx.wear.watchface.complications.data.ComplicationText
@@ -49,7 +50,7 @@
)
}
- override fun getPreviewData(type: ComplicationType) = when (type) {
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
ComplicationType.SHORT_TEXT ->
ShortTextComplicationData.Builder(
plainText("# 123"),
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt
new file mode 100644
index 0000000..c00411d
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/NonInterpolatedColorRampDataSourceService.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [RangedValueComplicationData] with a non-interpolated ramp. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class NonInterpolatedColorRampDataSourceService : ComplicationDataSourceService() {
+
+ override fun onComplicationRequest(
+ request: ComplicationRequest,
+ listener: ComplicationRequestListener
+ ) {
+ listener.onComplicationData(
+ RangedValueComplicationData.Builder(
+ value = 75f,
+ min = 0.0f,
+ max = 100.0f,
+ plainText("Example")
+ ).setText(plainText("Example"))
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(Color.GREEN, Color.YELLOW, Color.RED),
+ interpolated = false
+ )
+ )
+ .build()
+ )
+ }
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+ ComplicationType.RANGED_VALUE ->
+ RangedValueComplicationData.Builder(
+ value = 10f,
+ min = 0.0f,
+ max = 100.0f,
+ plainText("Example")
+ ).setText(plainText("Example"))
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(Color.GREEN, Color.YELLOW, Color.RED),
+ interpolated = false
+ )
+ )
+ .build()
+
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
index 1e9acb7..f4a625e 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/SynchronousDataSourceService.kt
@@ -74,21 +74,19 @@
)
}
- override fun getPreviewData(type: ComplicationType): ComplicationData? {
- return when (type) {
- ComplicationType.SHORT_TEXT ->
- ShortTextComplicationData.Builder(
- plainText("S 10"),
- ComplicationText.EMPTY
- ).build()
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+ ComplicationType.SHORT_TEXT ->
+ ShortTextComplicationData.Builder(
+ plainText("S 10"),
+ ComplicationText.EMPTY
+ ).build()
- ComplicationType.LONG_TEXT ->
- LongTextComplicationData.Builder(
- plainText("Secs 10"),
- ComplicationText.EMPTY
- ).build()
+ ComplicationType.LONG_TEXT ->
+ LongTextComplicationData.Builder(
+ plainText("Secs 10"),
+ ComplicationText.EMPTY
+ ).build()
- else -> null
- }
+ else -> null
}
}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt
new file mode 100644
index 0000000..073af3f
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/java/androidx/wear/watchface/complications/datasource/samples/WeightedElementDataSourceService.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.complications.datasource.samples
+
+import android.graphics.Color
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
+import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService
+import androidx.wear.watchface.complications.datasource.ComplicationRequest
+
+/** Trivial example of serving [WeightedElementsComplicationData]. */
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
+class WeightedElementDataSourceService : ComplicationDataSourceService() {
+
+ override fun onComplicationRequest(
+ request: ComplicationRequest,
+ listener: ComplicationRequestListener
+ ) {
+ listener.onComplicationData(
+ WeightedElementsComplicationData.Builder(
+ listOf(
+ WeightedElementsComplicationData.Element(1.0f, Color.RED),
+ WeightedElementsComplicationData.Element(1.0f, Color.GREEN),
+ WeightedElementsComplicationData.Element(2.0f, Color.BLUE),
+ WeightedElementsComplicationData.Element(3.0f, Color.YELLOW)
+ ),
+ plainText("Example weighted elements")
+ ).setText(plainText("Calories"))
+ .build()
+ )
+ }
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? = when (type) {
+ ComplicationType.WEIGHTED_ELEMENTS ->
+ WeightedElementsComplicationData.Builder(
+ listOf(
+ WeightedElementsComplicationData.Element(1.0f, Color.RED),
+ WeightedElementsComplicationData.Element(2.0f, Color.GREEN),
+ WeightedElementsComplicationData.Element(3.0f, Color.BLUE),
+ ),
+ plainText("Example weighted elements")
+ ).setText(plainText("Calories"))
+ .build()
+
+ else -> null
+ }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml b/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
index 814f7ca..84c8731 100644
--- a/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface-complications-data-source-samples/src/main/res/values/strings.xml
@@ -22,6 +22,10 @@
<string name="configurable_data_source_name" translatable="false">Configurable</string>
<string name="immediate_data_source_name" translatable="false">Immediate</string>
<string name="synchronous_data_source_name" translatable="false">Synchronous</string>
+ <string name="color_ramp_data_source_name" translatable="false">Color Ramp</string>
+ <string name="color_ramp2_data_source_name" translatable="false">Non interpolated color ramp</string>
+ <string name="goal_progress_data_source_name" translatable="false">Goal Progress</string>
+ <string name="weighted_elements_data_source_name" translatable="false">Weighted Elements</string>
<string name="config_title" translatable="false">Configuration</string>
<string name="config_button_title" translatable="false">Generate</string>
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index d6cae24..1aea238 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -1,6 +1,14 @@
// Signature format: 4.0
package androidx.wear.watchface.complications.data {
+ public final class ColorRamp {
+ ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
+ method public int[] getColors();
+ method public boolean getInterpolated();
+ property public final int[] colors;
+ property public final boolean interpolated;
+ }
+
public abstract sealed class ComplicationData {
method public final android.content.ComponentName? getDataSource();
method public final int getDisplayPolicy();
@@ -50,6 +58,7 @@
method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
@@ -59,6 +68,7 @@
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
}
public final class CountDownTimeReference {
@@ -81,6 +91,42 @@
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+ method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+ method public float getTargetValue();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+ method public float getValue();
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+ property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+ property public final float targetValue;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+ property public final float value;
+ field public static final float PLACEHOLDER;
+ field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+ }
+
+ public static final class GoalProgressComplicationData.Builder {
+ ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+ method public final T setDataSource(android.content.ComponentName? dataSource);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ }
+
public final class ImageKt {
}
@@ -214,6 +260,7 @@
}
public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public float getMax();
method public float getMin();
@@ -222,6 +269,8 @@
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
method public float getValue();
+ method public int getValueType();
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
property public final float max;
property public final float min;
@@ -230,6 +279,7 @@
property public final androidx.wear.watchface.complications.data.ComplicationText? text;
property public final androidx.wear.watchface.complications.data.ComplicationText? title;
property public final float value;
+ property public final int valueType;
field public static final float PLACEHOLDER;
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
@@ -237,6 +287,7 @@
public static final class RangedValueComplicationData.Builder {
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -246,6 +297,13 @@
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+ }
+
+ public final class RangedValueTypes {
+ field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
+ field public static final int SCORE = 1; // 0x1
+ field public static final int UNDEFINED = 0; // 0x0
}
public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
@@ -392,6 +450,53 @@
public final class TypeKt {
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method public int getElementBackgroundColor();
+ method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
+ method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+ method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+ property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property public final int elementBackgroundColor;
+ property public final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements;
+ property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+ property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+ field public static final androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Companion Companion;
+ field public static final int MAX_NUM_ELEMENTS = 20; // 0x14
+ field public static final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> PLACEHOLDER;
+ field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+ }
+
+ public static final class WeightedElementsComplicationData.Builder {
+ ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
+ method public final T setDataSource(android.content.ComponentName? dataSource);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ }
+
+ public static final class WeightedElementsComplicationData.Companion {
+ }
+
+ public static final class WeightedElementsComplicationData.Element {
+ ctor public WeightedElementsComplicationData.Element(@FloatRange(from=0.0, fromInclusive=false) float weight, @ColorInt int color);
+ method public int getColor();
+ method public float getWeight();
+ property public final int color;
+ property public final float weight;
+ }
+
}
package androidx.wear.watchface.utility {
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 8f00efe..6a990c8 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
// Signature format: 4.0
package androidx.wear.watchface.complications.data {
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ColorRamp {
+ public final class ColorRamp {
ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
method public int[] getColors();
method public boolean getInterpolated();
@@ -61,19 +61,17 @@
method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
- enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
- enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType LIST;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NO_DATA;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NO_PERMISSION;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType PHOTO_IMAGE;
- enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType PROTO_LAYOUT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
- enum_constant @androidx.wear.watchface.complications.data.ComplicationExperimental public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
}
public final class CountDownTimeReference {
@@ -96,8 +94,8 @@
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
@@ -105,7 +103,7 @@
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
method public float getValue();
- property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
@@ -120,7 +118,7 @@
public static final class GoalProgressComplicationData.Builder {
ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -135,34 +133,6 @@
public final class ImageKt {
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ListComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
- method public java.util.List<androidx.wear.watchface.complications.data.ComplicationData> getComplicationList();
- method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
- method public androidx.wear.watchface.complications.data.ListComplicationData.StyleHint getStyleHint();
- property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationData> complicationList;
- property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
- property public final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint;
- field public static final int MAX_ITEMS = 5; // 0x5
- field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
- }
-
- public static final class ListComplicationData.Builder {
- ctor public ListComplicationData.Builder(java.util.List<? extends androidx.wear.watchface.complications.data.ComplicationData> complicationList, androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- method public androidx.wear.watchface.complications.data.ListComplicationData build();
- method public final T setDataSource(android.content.ComponentName? dataSource);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
- method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
- method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
- }
-
- @androidx.wear.watchface.complications.data.ComplicationExperimental public enum ListComplicationData.StyleHint {
- method public static androidx.wear.watchface.complications.data.ListComplicationData.StyleHint valueOf(String name) throws java.lang.IllegalArgumentException;
- method public static androidx.wear.watchface.complications.data.ListComplicationData.StyleHint[] values();
- enum_constant public static final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint ColumnOfRows;
- enum_constant public static final androidx.wear.watchface.complications.data.ListComplicationData.StyleHint RowOfColumns;
- }
-
public final class LongTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -292,31 +262,8 @@
method public androidx.wear.watchface.complications.data.PlainComplicationText build();
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class ProtoLayoutComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
- method public androidx.wear.tiles.LayoutElementBuilders.Layout getAmbientLayout();
- method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
- method public androidx.wear.tiles.LayoutElementBuilders.Layout getInteractiveLayout();
- method public androidx.wear.tiles.ResourceBuilders.Resources getLayoutResources();
- property public final androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout;
- property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
- property public final androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout;
- property public final androidx.wear.tiles.ResourceBuilders.Resources layoutResources;
- field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
- }
-
- public static final class ProtoLayoutComplicationData.Builder {
- ctor public ProtoLayoutComplicationData.Builder(byte[] ambientLayout, byte[] interactiveLayout, byte[] layoutResources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- ctor public ProtoLayoutComplicationData.Builder(androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout, androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout, androidx.wear.tiles.ResourceBuilders.Resources resources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
- method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData build();
- method public final T setDataSource(android.content.ComponentName? dataSource);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
- method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
- method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
- method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
- }
-
public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public float getMax();
method public float getMin();
@@ -325,8 +272,8 @@
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
method public float getValue();
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public int getValueType();
- property @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+ method public int getValueType();
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
property public final float max;
property public final float min;
@@ -335,7 +282,7 @@
property public final androidx.wear.watchface.complications.data.ComplicationText? text;
property public final androidx.wear.watchface.complications.data.ComplicationText? title;
property public final float value;
- property @androidx.wear.watchface.complications.data.ComplicationExperimental public final int valueType;
+ property public final int valueType;
field public static final float PLACEHOLDER;
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
@@ -343,7 +290,7 @@
public static final class RangedValueComplicationData.Builder {
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -353,10 +300,10 @@
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
- method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class RangedValueTypes {
+ public final class RangedValueTypes {
field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
field public static final int SCORE = 1; // 0x1
field public static final int UNDEFINED = 0; // 0x0
@@ -506,7 +453,7 @@
public final class TypeKt {
}
- @androidx.wear.watchface.complications.data.ComplicationExperimental public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public int getElementBackgroundColor();
method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index e27cc62..4f6ce72 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -1,6 +1,14 @@
// Signature format: 4.0
package androidx.wear.watchface.complications.data {
+ public final class ColorRamp {
+ ctor public ColorRamp(@ColorInt int[] colors, boolean interpolated);
+ method public int[] getColors();
+ method public boolean getInterpolated();
+ property public final int[] colors;
+ property public final boolean interpolated;
+ }
+
public abstract sealed class ComplicationData {
method public final android.content.ComponentName? getDataSource();
method public final int getDisplayPolicy();
@@ -50,6 +58,7 @@
method public static androidx.wear.watchface.complications.data.ComplicationType valueOf(String name) throws java.lang.IllegalArgumentException;
method public static androidx.wear.watchface.complications.data.ComplicationType[] values();
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType EMPTY;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType GOAL_PROGRESS;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType LONG_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType MONOCHROMATIC_IMAGE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType NOT_CONFIGURED;
@@ -59,6 +68,7 @@
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType RANGED_VALUE;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SHORT_TEXT;
enum_constant public static final androidx.wear.watchface.complications.data.ComplicationType SMALL_IMAGE;
+ enum_constant @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final androidx.wear.watchface.complications.data.ComplicationType WEIGHTED_ELEMENTS;
}
public final class CountDownTimeReference {
@@ -81,6 +91,42 @@
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class GoalProgressComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+ method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+ method public float getTargetValue();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+ method public float getValue();
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+ property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+ property public final float targetValue;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+ property public final float value;
+ field public static final float PLACEHOLDER;
+ field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+ }
+
+ public static final class GoalProgressComplicationData.Builder {
+ ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
+ method public final T setDataSource(android.content.ComponentName? dataSource);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+ method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ }
+
public final class ImageKt {
}
@@ -215,6 +261,7 @@
}
public final class RangedValueComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ColorRamp? getColorRamp();
method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
method public float getMax();
method public float getMin();
@@ -223,6 +270,8 @@
method public androidx.wear.watchface.complications.data.ComplicationText? getText();
method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
method public float getValue();
+ method public int getValueType();
+ property public final androidx.wear.watchface.complications.data.ColorRamp? colorRamp;
property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
property public final float max;
property public final float min;
@@ -231,6 +280,7 @@
property public final androidx.wear.watchface.complications.data.ComplicationText? text;
property public final androidx.wear.watchface.complications.data.ComplicationText? title;
property public final float value;
+ property public final int valueType;
field public static final float PLACEHOLDER;
field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
}
@@ -238,6 +288,7 @@
public static final class RangedValueComplicationData.Builder {
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
@@ -247,6 +298,13 @@
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setValueType(int valueType);
+ }
+
+ public final class RangedValueTypes {
+ field public static final androidx.wear.watchface.complications.data.RangedValueTypes INSTANCE;
+ field public static final int SCORE = 1; // 0x1
+ field public static final int UNDEFINED = 0; // 0x0
}
public final class ShortTextComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
@@ -395,6 +453,53 @@
public final class TypeKt {
}
+ @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final class WeightedElementsComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
+ method public androidx.wear.watchface.complications.data.ComplicationText? getContentDescription();
+ method public int getElementBackgroundColor();
+ method public java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> getElements();
+ method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
+ method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getText();
+ method public androidx.wear.watchface.complications.data.ComplicationText? getTitle();
+ property public final androidx.wear.watchface.complications.data.ComplicationText? contentDescription;
+ property public final int elementBackgroundColor;
+ property public final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements;
+ property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
+ property public final androidx.wear.watchface.complications.data.SmallImage? smallImage;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? text;
+ property public final androidx.wear.watchface.complications.data.ComplicationText? title;
+ field public static final androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Companion Companion;
+ field public static final int MAX_NUM_ELEMENTS = 20; // 0x14
+ field public static final java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> PLACEHOLDER;
+ field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
+ }
+
+ public static final class WeightedElementsComplicationData.Builder {
+ ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
+ method public final T setDataSource(android.content.ComponentName? dataSource);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
+ method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
+ }
+
+ public static final class WeightedElementsComplicationData.Companion {
+ }
+
+ public static final class WeightedElementsComplicationData.Element {
+ ctor public WeightedElementsComplicationData.Element(@FloatRange(from=0.0, fromInclusive=false) float weight, @ColorInt int color);
+ method public int getColor();
+ method public float getWeight();
+ property public final int color;
+ property public final float weight;
+ }
+
}
package androidx.wear.watchface.utility {
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index c574d08..a9e5904 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -33,7 +33,6 @@
implementation("androidx.core:core:1.1.0")
implementation("androidx.preference:preference:1.1.0")
implementation("androidx.annotation:annotation:1.2.0")
- implementation("androidx.wear.tiles:tiles:1.1.0-alpha08")
testImplementation(libs.testCore)
testImplementation(libs.testRunner)
testImplementation(libs.testRules)
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index f174b51..5c954de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -93,6 +93,8 @@
TYPE_LARGE_IMAGE,
TYPE_NO_PERMISSION,
TYPE_NO_DATA,
+ TYPE_GOAL_PROGRESS,
+ TYPE_WEIGHTED_ELEMENTS,
EXP_TYPE_PROTO_LAYOUT,
EXP_TYPE_LIST
})
@@ -230,14 +232,6 @@
*/
public static final int TYPE_NO_PERMISSION = 9;
- // The following types are experimental, and they have negative IDs.
-
- /** Type that specifies a proto layout based complication. */
- public static final int EXP_TYPE_PROTO_LAYOUT = -11;
-
- /** Type that specifies a list of complication values. E.g. to support linear 3. */
- public static final int EXP_TYPE_LIST = -12;
-
/**
* Type used for complications which indicate progress towards a goal. The value may be
* accompanied by an icon and/or short text and title.
@@ -250,7 +244,7 @@
* text</i> fields are optional for this type, but at least one must be defined. The watch face
* may choose which of these fields to display, if any.
*/
- public static final int EXP_TYPE_GOAL_PROGRESS = -13;
+ public static final int TYPE_GOAL_PROGRESS = 13;
/**
* Type used for complications to display a series of weighted values e.g. in a pie chart. The
@@ -263,7 +257,15 @@
* text</i> fields are optional for this type, but at least one must be defined. The watch face
* may choose which of these fields to display, if any.
*/
- public static final int EXP_TYPE_WEIGHTED_ELEMENTS = -14;
+ public static final int TYPE_WEIGHTED_ELEMENTS = 14;
+
+ // The following types are experimental, and they have negative IDs.
+
+ /** Type that specifies a proto layout based complication. */
+ public static final int EXP_TYPE_PROTO_LAYOUT = -11;
+
+ /** Type that specifies a list of complication values. E.g. to support linear 3. */
+ public static final int EXP_TYPE_LIST = -12;
/** @hide */
@IntDef({IMAGE_STYLE_PHOTO, IMAGE_STYLE_ICON})
@@ -289,8 +291,13 @@
*/
public static final int IMAGE_STYLE_ICON = 2;
+ private static final String FIELD_COLOR_RAMP = "COLOR_RAMP";
+ private static final String FIELD_COLOR_RAMP_INTERPOLATED = "COLOR_RAMP_INTERPOLATED";
private static final String FIELD_DATA_SOURCE = "FIELD_DATA_SOURCE";
private static final String FIELD_DISPLAY_POLICY = "DISPLAY_POLICY";
+ private static final String FIELD_ELEMENT_BACKGROUND_COLOR = "ELEMENT_BACKGROUND_COLOR";
+ private static final String FIELD_ELEMENT_COLORS = "ELEMENT_COLORS";
+ private static final String FIELD_ELEMENT_WEIGHTS = "ELEMENT_WEIGHTS";
private static final String FIELD_END_TIME = "END_TIME";
private static final String FIELD_ICON = "ICON";
private static final String FIELD_ICON_BURN_IN_PROTECTION = "ICON_BURN_IN_PROTECTION";
@@ -311,6 +318,7 @@
private static final String FIELD_START_TIME = "START_TIME";
private static final String FIELD_TAP_ACTION = "TAP_ACTION";
private static final String FIELD_TAP_ACTION_LOST = "FIELD_TAP_ACTION_LOST";
+ private static final String FIELD_TARGET_VALUE = "TARGET_VALUE";
private static final String FIELD_TIMELINE_START_TIME = "TIMELINE_START_TIME";
private static final String FIELD_TIMELINE_END_TIME = "TIMELINE_END_TIME";
private static final String FIELD_TIMELINE_ENTRIES = "TIMELINE";
@@ -319,11 +327,6 @@
private static final String FIELD_VALUE_TYPE = "VALUE_TYPE";
// Experimental fields, these are subject to change without notice.
- private static final String EXP_FIELD_COLOR_RAMP = "EXP_COLOR_RAMP";
- private static final String EXP_FIELD_COLOR_RAMP_INTERPOLATED = "EXP_COLOR_RAMP_INTERPOLATED";
- private static final String EXP_FIELD_ELEMENT_BACKGROUND_COLOR = "EXP_ELEMENT_BACKGROUND_COLOR";
- private static final String EXP_FIELD_ELEMENT_COLORS = "EXP_ELEMENT_COLORS";
- private static final String EXP_FIELD_ELEMENT_WEIGHTS = "EXP_ELEMENT_WEIGHTS";
private static final String EXP_FIELD_LIST_ENTRIES = "EXP_LIST_ENTRIES";
private static final String EXP_FIELD_LIST_ENTRY_TYPE = "EXP_LIST_ENTRY_TYPE";
private static final String EXP_FIELD_LIST_STYLE_HINT = "EXP_LIST_STYLE_HINT";
@@ -332,7 +335,6 @@
"EXP_FIELD_PROTO_LAYOUT_INTERACTIVE";
private static final String EXP_FIELD_PROTO_LAYOUT_RESOURCES =
"EXP_FIELD_PROTO_LAYOUT_RESOURCES";
- private static final String EXP_FIELD_TARGET_VALUE = "EXP_TARGET_VALUE";
// Originally it was planned to support both content and image content descriptions.
private static final String FIELD_CONTENT_DESCRIPTION = "IMAGE_CONTENT_DESCRIPTION";
@@ -354,8 +356,8 @@
set.add(TYPE_NO_DATA);
set.add(EXP_TYPE_PROTO_LAYOUT);
set.add(EXP_TYPE_LIST);
- set.add(EXP_TYPE_GOAL_PROGRESS);
- set.add(EXP_TYPE_WEIGHTED_ELEMENTS);
+ set.add(TYPE_GOAL_PROGRESS);
+ set.add(TYPE_WEIGHTED_ELEMENTS);
return set;
}
@@ -379,10 +381,10 @@
EXP_FIELD_PROTO_LAYOUT_INTERACTIVE,
EXP_FIELD_PROTO_LAYOUT_RESOURCES});
map.put(EXP_TYPE_LIST, new String[]{EXP_FIELD_LIST_ENTRIES});
- map.put(EXP_TYPE_GOAL_PROGRESS, new String[]{FIELD_VALUE, EXP_FIELD_TARGET_VALUE});
- map.put(EXP_TYPE_WEIGHTED_ELEMENTS, new String[]{EXP_FIELD_ELEMENT_WEIGHTS,
- EXP_FIELD_ELEMENT_COLORS,
- EXP_FIELD_ELEMENT_BACKGROUND_COLOR});
+ map.put(TYPE_GOAL_PROGRESS, new String[]{FIELD_VALUE, FIELD_TARGET_VALUE});
+ map.put(TYPE_WEIGHTED_ELEMENTS, new String[]{FIELD_ELEMENT_WEIGHTS,
+ FIELD_ELEMENT_COLORS,
+ FIELD_ELEMENT_BACKGROUND_COLOR});
return map;
}
@@ -429,8 +431,8 @@
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
- EXP_FIELD_COLOR_RAMP,
- EXP_FIELD_COLOR_RAMP_INTERPOLATED,
+ FIELD_COLOR_RAMP,
+ FIELD_COLOR_RAMP_INTERPOLATED,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY,
FIELD_VALUE_TYPE});
@@ -503,7 +505,7 @@
FIELD_DATA_SOURCE,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY});
- map.put(EXP_TYPE_GOAL_PROGRESS, new String[]{
+ map.put(TYPE_GOAL_PROGRESS, new String[]{
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
FIELD_ICON,
@@ -514,11 +516,11 @@
FIELD_TAP_ACTION,
FIELD_CONTENT_DESCRIPTION,
FIELD_DATA_SOURCE,
- EXP_FIELD_COLOR_RAMP,
- EXP_FIELD_COLOR_RAMP_INTERPOLATED,
+ FIELD_COLOR_RAMP,
+ FIELD_COLOR_RAMP_INTERPOLATED,
FIELD_PERSISTENCE_POLICY,
FIELD_DISPLAY_POLICY});
- map.put(EXP_TYPE_WEIGHTED_ELEMENTS, new String[]{
+ map.put(TYPE_WEIGHTED_ELEMENTS, new String[]{
FIELD_SHORT_TEXT,
FIELD_SHORT_TITLE,
FIELD_ICON,
@@ -640,10 +642,10 @@
if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
oos.writeFloat(mComplicationData.getRangedMaxValue());
}
- if (isFieldValidForType(EXP_FIELD_TARGET_VALUE, type)) {
+ if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
oos.writeFloat(mComplicationData.getTargetValue());
}
- if (isFieldValidForType(EXP_FIELD_COLOR_RAMP, type)) {
+ if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
int[] colors = mComplicationData.getColorRamp();
if (colors != null) {
oos.writeBoolean(true);
@@ -655,7 +657,7 @@
oos.writeBoolean(false);
}
}
- if (isFieldValidForType(EXP_FIELD_COLOR_RAMP_INTERPOLATED, type)) {
+ if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
Boolean isColorRampSmoothShaded = mComplicationData.isColorRampInterpolated();
if (isColorRampSmoothShaded != null) {
oos.writeBoolean(true);
@@ -664,7 +666,7 @@
oos.writeBoolean(false);
}
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_WEIGHTS, type)) {
+ if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
float[] weights = mComplicationData.getElementWeights();
if (weights != null) {
oos.writeBoolean(true);
@@ -676,7 +678,7 @@
oos.writeBoolean(false);
}
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_COLORS, type)) {
+ if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
int[] colors = mComplicationData.getElementColors();
if (colors != null) {
oos.writeBoolean(true);
@@ -688,7 +690,7 @@
oos.writeBoolean(false);
}
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+ if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
oos.writeInt(mComplicationData.getElementBackgroundColor());
}
if (isFieldValidForType(FIELD_START_TIME, type)) {
@@ -842,38 +844,38 @@
if (isFieldValidForType(FIELD_MAX_VALUE, type)) {
fields.putFloat(FIELD_MAX_VALUE, ois.readFloat());
}
- if (isFieldValidForType(EXP_FIELD_TARGET_VALUE, type)) {
- fields.putFloat(EXP_FIELD_TARGET_VALUE, ois.readFloat());
+ if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
+ fields.putFloat(FIELD_TARGET_VALUE, ois.readFloat());
}
- if (isFieldValidForType(EXP_FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
+ if (isFieldValidForType(FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
int numColors = ois.readInt();
int[] colors = new int[numColors];
for (int i = 0; i < numColors; ++i) {
colors[i] = ois.readInt();
}
- fields.putIntArray(EXP_FIELD_COLOR_RAMP, colors);
+ fields.putIntArray(FIELD_COLOR_RAMP, colors);
}
- if (isFieldValidForType(EXP_FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
- fields.putBoolean(EXP_FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean());
+ if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
+ fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean());
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
+ if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
int numWeights = ois.readInt();
float[] weights = new float[numWeights];
for (int i = 0; i < numWeights; ++i) {
weights[i] = ois.readFloat();
}
- fields.putFloatArray(EXP_FIELD_ELEMENT_WEIGHTS, weights);
+ fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, weights);
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
+ if (isFieldValidForType(FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
int numColors = ois.readInt();
int[] colors = new int[numColors];
for (int i = 0; i < numColors; ++i) {
colors[i] = ois.readInt();
}
- fields.putIntArray(EXP_FIELD_ELEMENT_COLORS, colors);
+ fields.putIntArray(FIELD_ELEMENT_COLORS, colors);
}
- if (isFieldValidForType(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
- fields.putInt(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt());
+ if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
+ fields.putInt(FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt());
}
if (isFieldValidForType(FIELD_START_TIME, type)) {
fields.putLong(FIELD_START_TIME, ois.readLong());
@@ -1253,7 +1255,7 @@
*/
public boolean hasTargetValue() {
try {
- return isFieldValidForType(EXP_FIELD_TARGET_VALUE, mType);
+ return isFieldValidForType(FIELD_TARGET_VALUE, mType);
} catch (BadParcelableException e) {
return false;
}
@@ -1262,26 +1264,26 @@
/**
* Returns the <i>value</i> field for this complication.
*
- * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_GOAL_PROGRESS}.
+ * <p>Valid only if the type of this complication data is {@link #TYPE_GOAL_PROGRESS}.
* Otherwise returns zero.
*/
public float getTargetValue() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_TARGET_VALUE, mType);
- return mFields.getFloat(EXP_FIELD_TARGET_VALUE);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_TARGET_VALUE, mType);
+ return mFields.getFloat(FIELD_TARGET_VALUE);
}
/**
* Returns the colors for the progress bar.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE} or
- * {@link #EXP_TYPE_GOAL_PROGRESS}.
+ * {@link #TYPE_GOAL_PROGRESS}.
*/
@ColorInt
@Nullable
public int[] getColorRamp() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_COLOR_RAMP, mType);
- if (mFields.containsKey(EXP_FIELD_COLOR_RAMP)) {
- return mFields.getIntArray(EXP_FIELD_COLOR_RAMP);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_COLOR_RAMP, mType);
+ if (mFields.containsKey(FIELD_COLOR_RAMP)) {
+ return mFields.getIntArray(FIELD_COLOR_RAMP);
}
return null;
}
@@ -1290,16 +1292,16 @@
* Returns either a boolean where: true means the color ramp colors should be smoothly
* interpolatded; false means the color ramp should be rendered in equal sized blocks of
* solid color; null means this value wasn't set, i.e. the complication is not of type
- * {@link #TYPE_RANGED_VALUE} or {@link #EXP_TYPE_GOAL_PROGRESS}.
+ * {@link #TYPE_RANGED_VALUE} or {@link #TYPE_GOAL_PROGRESS}.
*
* <p>Valid only if the type of this complication data is {@link #TYPE_RANGED_VALUE} or
- * {@link #EXP_TYPE_GOAL_PROGRESS}.
+ * {@link #TYPE_GOAL_PROGRESS}.
*/
@Nullable
public Boolean isColorRampInterpolated() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_COLOR_RAMP_INTERPOLATED, mType);
- if (mFields.containsKey(EXP_FIELD_COLOR_RAMP_INTERPOLATED)) {
- return mFields.getBoolean(EXP_FIELD_COLOR_RAMP_INTERPOLATED);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_COLOR_RAMP_INTERPOLATED, mType);
+ if (mFields.containsKey(FIELD_COLOR_RAMP_INTERPOLATED)) {
+ return mFields.getBoolean(FIELD_COLOR_RAMP_INTERPOLATED);
}
return null;
}
@@ -1683,37 +1685,37 @@
/**
* Returns the element weights for this complication.
*
- * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+ * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
* Otherwise returns null.
*/
@Nullable
public float[] getElementWeights() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_WEIGHTS, mType);
- return mFields.getFloatArray(EXP_FIELD_ELEMENT_WEIGHTS);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_WEIGHTS, mType);
+ return mFields.getFloatArray(FIELD_ELEMENT_WEIGHTS);
}
/**
* Returns the element colors for this complication.
*
- * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+ * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
* Otherwise returns null.
*/
@Nullable
public int[] getElementColors() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_COLORS, mType);
- return mFields.getIntArray(EXP_FIELD_ELEMENT_COLORS);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_COLORS, mType);
+ return mFields.getIntArray(FIELD_ELEMENT_COLORS);
}
/**
* Returns the background color to use between elements for this complication.
*
- * <p>Valid only if the type of this complication data is {@link #EXP_TYPE_WEIGHTED_ELEMENTS}.
+ * <p>Valid only if the type of this complication data is {@link #TYPE_WEIGHTED_ELEMENTS}.
* Otherwise returns 0.
*/
@ColorInt
public int getElementBackgroundColor() {
- checkFieldValidForTypeWithoutThrowingException(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, mType);
- return mFields.getInt(EXP_FIELD_ELEMENT_BACKGROUND_COLOR);
+ checkFieldValidForTypeWithoutThrowingException(FIELD_ELEMENT_BACKGROUND_COLOR, mType);
+ return mFields.getInt(FIELD_ELEMENT_BACKGROUND_COLOR);
}
/**
@@ -1978,12 +1980,12 @@
/**
* Sets the <i>value</i> field. This is required for the {@link #TYPE_RANGED_VALUE} type,
- * and the {@link #EXP_TYPE_GOAL_PROGRESS} type. For {@link #TYPE_RANGED_VALUE} value must
- * be in the range [min .. max] for {@link #EXP_TYPE_GOAL_PROGRESS} value must be >= and may
+ * and the {@link #TYPE_GOAL_PROGRESS} type. For {@link #TYPE_RANGED_VALUE} value must
+ * be in the range [min .. max] for {@link #TYPE_GOAL_PROGRESS} value must be >= and may
* be greater than target value.
*
* <p>Both the {@link #TYPE_RANGED_VALUE} complication and the
- * {@link #EXP_TYPE_GOAL_PROGRESS} complication visually present a single value, which is
+ * {@link #TYPE_GOAL_PROGRESS} complication visually present a single value, which is
* usually a percentage. E.g. you have completed 70% of today's target of 10000 steps.
*
* <p>Returns this Builder to allow chaining.
@@ -2040,8 +2042,8 @@
/**
* Sets the <i>targetValue</i> field. This is required for the
- * {@link #EXP_TYPE_GOAL_PROGRESS} type, and is not valid for any other type. A
- * {@link #EXP_TYPE_GOAL_PROGRESS} complication visually presents a single value, which
+ * {@link #TYPE_GOAL_PROGRESS} type, and is not valid for any other type. A
+ * {@link #TYPE_GOAL_PROGRESS} complication visually presents a single value, which
* is usually a percentage. E.g. you have completed 70% of today's target of 10000 steps.
*
* <p>Returns this Builder to allow chaining.
@@ -2050,7 +2052,7 @@
*/
@NonNull
public Builder setTargetValue(float targetValue) {
- putFloatField(EXP_FIELD_TARGET_VALUE, targetValue);
+ putFloatField(FIELD_TARGET_VALUE, targetValue);
return this;
}
@@ -2377,7 +2379,7 @@
*/
@NonNull
public Builder setColorRamp(@Nullable int[] colorRamp) {
- putOrRemoveField(EXP_FIELD_COLOR_RAMP, colorRamp);
+ putOrRemoveField(FIELD_COLOR_RAMP, colorRamp);
return this;
}
@@ -2389,7 +2391,7 @@
*/
@NonNull
public Builder setColorRampIsSmoothShaded(@Nullable Boolean isSmoothShaded) {
- putOrRemoveField(EXP_FIELD_COLOR_RAMP_INTERPOLATED, isSmoothShaded);
+ putOrRemoveField(FIELD_COLOR_RAMP_INTERPOLATED, isSmoothShaded);
return this;
}
@@ -2424,7 +2426,7 @@
*/
@NonNull
public Builder setElementWeights(@Nullable float[] elementWeights) {
- putOrRemoveField(EXP_FIELD_ELEMENT_WEIGHTS, elementWeights);
+ putOrRemoveField(FIELD_ELEMENT_WEIGHTS, elementWeights);
return this;
}
@@ -2435,7 +2437,7 @@
*/
@NonNull
public Builder setElementColors(@Nullable int[] elementColors) {
- putOrRemoveField(EXP_FIELD_ELEMENT_COLORS, elementColors);
+ putOrRemoveField(FIELD_ELEMENT_COLORS, elementColors);
return this;
}
@@ -2446,7 +2448,7 @@
*/
@NonNull
public Builder setElementBackgroundColor(@ColorInt int elementBackgroundColor) {
- putOrRemoveField(EXP_FIELD_ELEMENT_BACKGROUND_COLOR, elementBackgroundColor);
+ putOrRemoveField(FIELD_ELEMENT_BACKGROUND_COLOR, elementBackgroundColor);
return this;
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index bdf8831..717fa98 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -27,8 +27,6 @@
import androidx.annotation.FloatRange
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
-import androidx.wear.tiles.LayoutElementBuilders
-import androidx.wear.tiles.ResourceBuilders
import java.time.Instant
/** The wire format for [ComplicationData]. */
@@ -267,7 +265,6 @@
null
)
- @OptIn(ComplicationExperimental::class)
val contentDescription: ComplicationText? =
when (placeholder) {
is ShortTextComplicationData -> placeholder.contentDescription
@@ -859,16 +856,19 @@
* overrides the normal watch face colors when there's a particular semantic meaning. E.g. red to
* blue for a ranged value representing temperature.
*
+ * Note this is a subset of the functionality of [android.graphics.LinearGradient] and the x & y
+ * coordinates for the ramp are not known to the complication data source.
+ *
* @property colors The colors to render the progress bar with. For [RangedValueComplicationData]
* the first color corresponds to [RangedValueComplicationData.min] and the last color to
* [RangedValueComplicationData.max]. For [GoalProgressComplicationData] the first color corresponds
* to zero and the last color to [GoalProgressComplicationData.targetValue]. A maximum of 7 colors
- * may be specified. When rendered the colors must be evenly spread along the progress bar.
+ * may be specified. When rendered the colors must be evenly spread along the progress bar. The
+ * colors must be meaningful to the user, e.g. blue = cold, red/yellow = warm.
* @property interpolated If `true` then the colors should be smoothly interpolated when rendering
* the progress bar. If `false` the colors should be rendered as equal sized regions of solid color,
* resulting in a noticeable step between each color.
*/
-@ComplicationExperimental
public class ColorRamp(
@ColorInt val colors: IntArray,
@get:JvmName("isInterpolated")
@@ -908,7 +908,6 @@
* renderer may wish to visually differentiate between the different types, for example rendering a
* dot on a line/arc to show the value for a [SCORE].
*/
-@ComplicationExperimental
object RangedValueTypes {
/** The ranged value's semantic hasn't been defined, but it's most commonly a percentage. */
const val UNDEFINED = 0
@@ -921,7 +920,6 @@
}
/** @hide */
-@OptIn(ComplicationExperimental::class)
@IntDef(
value = [
RangedValueTypes.UNDEFINED,
@@ -977,9 +975,11 @@
* placeholder rather than rendering normally, its suggested it should be rendered as a light grey
* box.
* @property contentDescription The content description field for accessibility.
+ * @property colorRamp Optional hint to render the value with the specified [ColorRamp]. When
+ * present the renderer may choose to use the ColorRamp when rendering the progress bar.
+ * @property valueType The semantic meaning of [value], see [RangedValueType] for more details.
*/
-public class RangedValueComplicationData @OptIn(ComplicationExperimental::class)
-internal constructor(
+public class RangedValueComplicationData internal constructor(
public val value: Float,
public val min: Float,
public val max: Float,
@@ -992,8 +992,8 @@
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?,
dataSource: ComponentName?,
- colorRamp: ColorRamp?,
- @RangedValueType valueType: Int,
+ public val colorRamp: ColorRamp?,
+ @RangedValueType public val valueType: Int,
@ComplicationPersistencePolicy persistencePolicy: Int,
@ComplicationDisplayPolicy displayPolicy: Int
) : ComplicationData(
@@ -1005,19 +1005,6 @@
persistencePolicy = persistencePolicy,
displayPolicy = displayPolicy
) {
- /** Optional hint to render the value with the specified [ColorRamp]. */
- @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ComplicationExperimental
- @ComplicationExperimental
- val colorRamp: ColorRamp? = colorRamp
-
- /** The semantic meaning of [value], see [RangedValueType] for more details. */
- @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ComplicationExperimental
- @ComplicationExperimental
- @RangedValueType
- val valueType: Int = valueType
-
/**
* Builder for [RangedValueComplicationData].
*
@@ -1030,7 +1017,6 @@
* @param max The maximum value. This must be less than [Float.MAX_VALUE].
* @param contentDescription Localized description for use by screen readers
*/
- @OptIn(ComplicationExperimental::class)
public class Builder(
private val value: Float,
private val min: Float,
@@ -1043,9 +1029,7 @@
private var smallImage: SmallImage? = null
private var title: ComplicationText? = null
private var text: ComplicationText? = null
- @OptIn(ComplicationExperimental::class)
private var colorRamp: ColorRamp? = null
- @OptIn(ComplicationExperimental::class)
@RangedValueType
private var valueType: Int = RangedValueTypes.UNDEFINED
@@ -1087,25 +1071,23 @@
}
/**
- * Sets an optional hint which suggests the renderer draws the complication using a
+ * Sets an optional hint that the renderer should draw the progress bar using the
* [ColorRamp].
*/
- @ComplicationExperimental
public fun setColorRamp(colorRamp: ColorRamp?): Builder = apply {
this.colorRamp = colorRamp
}
/**
- * Sets the semantic meaning of [value], see [@RangedValueTypes] for detail.s Defaults to
- * [RangedValueTypes.UNDEFINED] if not set.
+ * Sets the semantic meaning of [value], see [@RangedValueTypes] for details. Defaults to
+ * [RangedValueTypes.UNDEFINED] if not set. Watch faces may use this as a hint to select
+ * rendering style.
*/
- @ComplicationExperimental
public fun setValueType(@RangedValueType valueType: Int): Builder = apply {
this.valueType = valueType
}
/** Builds the [RangedValueComplicationData]. */
- @OptIn(ComplicationExperimental::class)
public override fun build(): RangedValueComplicationData {
require(
monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1144,7 +1126,6 @@
}.build().also { cachedWireComplicationData = it }
}
- @OptIn(ComplicationExperimental::class)
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
builder.setRangedMinValue(min)
@@ -1169,7 +1150,6 @@
builder.setRangedValueType(valueType)
}
- @OptIn(ComplicationExperimental::class)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -1196,7 +1176,6 @@
return true
}
- @OptIn(ComplicationExperimental::class)
override fun hashCode(): Int {
var result = value.hashCode()
result = 31 * result + valueType
@@ -1217,7 +1196,6 @@
return result
}
- @OptIn(ComplicationExperimental::class)
override fun toString(): String {
val valueString = if (WireComplicationData.shouldRedact()) {
"REDACTED"
@@ -1267,10 +1245,12 @@
}
/**
- * Type used for complications which show progress towards a goal, E.g. you've done 2400 out of your
- * daily target of 10000 steps. Unlike [RangedValueComplicationData] [value] is allowed to be larger
- * than [targetValue] (e.g. you've done 12000 steps) and renderers may chose to acknowledge this in
- * a special way. The value may be accompanied by an icon and/or short text and title.
+ * Type used for complications which shows the user's progress towards a goal, E.g. you've done 2400
+ * out of your daily target of 10000 steps. Unlike [RangedValueComplicationData] [value] is allowed
+ * to be larger than [targetValue] (e.g. you've done 12000 steps) and renderers may chose to
+ * acknowledge this in a special way (e.g. by colorizing part of the progress bar in a different
+ * color to indicate progress past the goal). The value may be accompanied by an icon and/or short
+ * text and title.
*
* The [value], and [targetValue] fields are required for this type and the progress is expected to
* always be displayed.
@@ -1285,8 +1265,8 @@
* specify both a [monochromaticImage] and a [smallImage].
*
* If you want to represent a score for something that's not based on the user (e.g. air quality
- * index) then you should use a [RangedValueComplicationData] and pass [RangedValueTypes.SCORE] into
- * [RangedValueComplicationData.Builder.setValueType].
+ * index) then you should instead use a [RangedValueComplicationData] and pass
+ * [RangedValueTypes.SCORE] into [RangedValueComplicationData.Builder.setValueType].
*
* @property value The [Float] value of this complication which is >= 0f, this value may be larger
* than [targetValue]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder
@@ -1318,8 +1298,10 @@
* placeholder rather than rendering normally, its suggested it should be rendered as a light grey
* box.
* @property contentDescription The content description field for accessibility.
+ * @property colorRamp Optional hint to render the progress bar representing [value] with the
+ * specified [ColorRamp].
*/
-@ComplicationExperimental
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class GoalProgressComplicationData
internal constructor(
public val value: Float,
@@ -1333,7 +1315,7 @@
validTimeRange: TimeRange?,
cachedWireComplicationData: WireComplicationData?,
dataSource: ComponentName?,
- colorRamp: ColorRamp?,
+ public val colorRamp: ColorRamp?,
@ComplicationPersistencePolicy persistencePolicy: Int,
@ComplicationDisplayPolicy displayPolicy: Int
) : ComplicationData(
@@ -1345,12 +1327,6 @@
persistencePolicy = persistencePolicy,
displayPolicy = displayPolicy
) {
- /** Optional hint to render the value with the specified [ColorRamp]. */
- @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
- @get:ComplicationExperimental
- @ComplicationExperimental
- val colorRamp: ColorRamp? = colorRamp
-
/**
* Builder for [GoalProgressComplicationData].
*
@@ -1361,7 +1337,6 @@
* @param targetValue The target value. This must be less than [Float.MAX_VALUE].
* @param contentDescription Localized description for use by screen readers
*/
- @OptIn(ComplicationExperimental::class)
public class Builder(
private val value: Float,
private val targetValue: Float,
@@ -1373,7 +1348,6 @@
private var smallImage: SmallImage? = null
private var title: ComplicationText? = null
private var text: ComplicationText? = null
- @OptIn(ComplicationExperimental::class)
private var colorRamp: ColorRamp? = null
init {
@@ -1417,13 +1391,11 @@
* Sets an optional hint which suggests the renderer draws the complication using a
* [ColorRamp].
*/
- @ComplicationExperimental
public fun setColorRamp(colorRamp: ColorRamp?): Builder = apply {
this.colorRamp = colorRamp
}
/** Builds the [GoalProgressComplicationData]. */
- @OptIn(ComplicationExperimental::class)
public override fun build(): GoalProgressComplicationData {
require(
monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1460,7 +1432,6 @@
}.build().also { cachedWireComplicationData = it }
}
- @OptIn(ComplicationExperimental::class)
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setRangedValue(value)
builder.setTargetValue(targetValue)
@@ -1483,7 +1454,6 @@
}
}
- @OptIn(ComplicationExperimental::class)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -1508,7 +1478,6 @@
return true
}
- @OptIn(ComplicationExperimental::class)
override fun hashCode(): Int {
var result = value.hashCode()
result = 31 * result + targetValue.hashCode()
@@ -1527,7 +1496,6 @@
return result
}
- @OptIn(ComplicationExperimental::class)
override fun toString(): String {
val valueString = if (WireComplicationData.shouldRedact()) {
"REDACTED"
@@ -1560,7 +1528,6 @@
/** @hide */
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
- @OptIn(ComplicationExperimental::class)
@JvmField
public val TYPE: ComplicationType = ComplicationType.GOAL_PROGRESS
@@ -1593,11 +1560,13 @@
* recommended to choose the [smallImage]. It's best practice for a ComplicationDataSource to
* specify both a [monochromaticImage] and a [smallImage].
*
- * @property elements The breakdown of the subject into various [Element]s. E.g. the proportion of
- * calories consumed which were carbohydrates, fats etc... If this is equal to [PLACEHOLDER] then
- * the renderer must display this in a visually distinct way to suggest to the user that it's
- * placeholder data. E.g. each rendered is rendered in light grey. The maximum valid size of this
- * list is [MAX_NUM_ELEMENTS].
+ * @property elements The breakdown of the subject into various [Element]s (e.g. the proportion of
+ * calories consumed which were carbohydrates, fats, etc.). The colors need to be meaningful to the
+ * user (e.g. blue is cold, yellow/red is worm), and should be consistent with the experience
+ * launched by tapping on the complication. If this is equal to [PLACEHOLDER] then the renderer must
+ * display this in a visually distinct way to suggest to the user that it's placeholder data. E.g.
+ * each element is rendered in light grey. The maximum valid size of this list is
+ * [MAX_NUM_ELEMENTS].
* @property elementBackgroundColor If elements are draw as segments then this is the background
* color to use in between them.
* @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -1626,7 +1595,7 @@
* box.
* @property contentDescription The content description field for accessibility.
*/
-@ComplicationExperimental
+@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class WeightedElementsComplicationData
internal constructor(
public val elements: List<Element>,
@@ -1657,10 +1626,11 @@
* @property weight The weight of the Element which must be > zero. The size of the element when
* rendered should be proportional to its weight. Weights are not required to sum to any
* particular value.
- * @property color The color of the Element. In conjunction with the other fields this color
- * needs to be meaningful to the user. Tapping on the complication should launch an experience
- * where the data is presented in more detail. Care must be taken to ensure the colors used are
- * consistent.
+ * @property color The color of the Element, which must be used instead of the watch face's
+ * colors. This color needs to be meaningful to the user in conjunction with the other fields
+ * (e.g. blue is cold, red/yellow is warm). Tapping on the complication should launch an
+ * experience where the data is presented in more detail. Care must be taken to ensure the
+ * colors used are consistent with the launched experience.
*/
class Element(
@FloatRange(from = 0.0, fromInclusive = false) val weight: Float,
@@ -1705,7 +1675,6 @@
* [MAX_NUM_ELEMENTS].
* @param contentDescription Localized description for use by screen readers
*/
- @OptIn(ComplicationExperimental::class)
public class Builder(
private val elements: List<Element>,
private var contentDescription: ComplicationText
@@ -1766,7 +1735,6 @@
}
/** Builds the [GoalProgressComplicationData]. */
- @OptIn(ComplicationExperimental::class)
public override fun build(): WeightedElementsComplicationData {
require(
monochromaticImage != null || smallImage != null || text != null || title != null
@@ -1802,7 +1770,6 @@
}.build().also { cachedWireComplicationData = it }
}
- @OptIn(ComplicationExperimental::class)
override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
builder.setElementWeights(elements.map { it.weight }.toFloatArray())
builder.setElementColors(elements.map { it.color }.toIntArray())
@@ -1832,7 +1799,6 @@
}
}
- @OptIn(ComplicationExperimental::class)
override fun toString(): String {
val elementsString = if (WireComplicationData.shouldRedact()) {
"REDACTED"
@@ -1888,7 +1854,6 @@
public companion object {
/** The [ComplicationType] corresponding to objects of this type. */
- @OptIn(ComplicationExperimental::class)
@JvmField
public val TYPE: ComplicationType = ComplicationType.WEIGHTED_ELEMENTS
@@ -2378,416 +2343,6 @@
}
/**
- * A complication that contains a serialized protoLayout.
- *
- * @property contentDescription The content description field for accessibility and is used to
- * describe what data the image represents. If the image is purely stylistic, and does not convey
- * any information to the user, then provide an empty content description. If no content description
- * is provided, a generic content description will be used instead.
- */
-@ComplicationExperimental
-public class ProtoLayoutComplicationData
-internal
-/**
- * @param ambientLayoutWireFormat The [LayoutElementBuilders.Layout] serialized into a
- * [ByteArray] to be displayed when the device is ambient.
- * @param interactiveLayoutWireFormat The [LayoutElementBuilders.Layout] serialized into a
- * [ByteArray] to be displayed when the device is interactive.
- * @param layoutResourcesWireFormat The [ResourceBuilders.Resources] serialized into a [ByteArray]
- * for [interactiveLayoutWireFormat] and [ambientLayoutWireFormat].
- */
-constructor(
- private val ambientLayoutWireFormat: ByteArray,
- private val interactiveLayoutWireFormat: ByteArray,
- private val layoutResourcesWireFormat: ByteArray,
- val contentDescription: ComplicationText?,
- tapAction: PendingIntent?,
- validTimeRange: TimeRange?,
- cachedWireComplicationData: WireComplicationData?,
- dataSource: ComponentName?,
- @ComplicationPersistencePolicy persistencePolicy: Int,
- @ComplicationDisplayPolicy displayPolicy: Int
-) : ComplicationData(
- TYPE,
- tapAction,
- cachedWireComplicationData,
- validTimeRange ?: TimeRange.ALWAYS,
- dataSource = dataSource,
- persistencePolicy = persistencePolicy,
- displayPolicy = displayPolicy
- ) {
-
- /** The [LayoutElementBuilders.Layout] to be displayed when the device is ambient. */
- public val ambientLayout: LayoutElementBuilders.Layout by lazy {
- LayoutElementBuilders.Layout.fromByteArray(ambientLayoutWireFormat)!!
- }
-
- /** The [LayoutElementBuilders.Layout] to be displayed when the device is interactive. */
- public val interactiveLayout: LayoutElementBuilders.Layout by lazy {
- LayoutElementBuilders.Layout.fromByteArray(interactiveLayoutWireFormat)!!
- }
-
- /** The [ResourceBuilders.Resources] for [ambientLayout] and [interactiveLayout]. */
- public val layoutResources: ResourceBuilders.Resources by lazy {
- ResourceBuilders.Resources.fromByteArray(layoutResourcesWireFormat)!!
- }
-
- /**
- * Builder for [ProtoLayoutComplicationData].
- *
- * You must at a minimum set the [ambientLayout], [interactiveLayout], [layoutResources] and
- * [contentDescription] fields.
- *
- * @param ambientLayout The [LayoutElementBuilders.Layout] serialized into a [ByteArray] to be
- * displayed when the device is ambient
- * @param interactiveLayout The [LayoutElementBuilders.Layout] serialized into a [ByteArray] to
- * be displayed when the device is interactive
- * @param layoutResources The [ResourceBuilders.Resources] serialized into a [ByteArray] for
- * [interactiveLayout] and [ambientLayout]
- * @param contentDescription Localized description for use by screen readers
- */
- public class Builder(
- private val ambientLayout: ByteArray,
- private val interactiveLayout: ByteArray,
- private val layoutResources: ByteArray,
- private val contentDescription: ComplicationText
- ) : BaseBuilder<Builder, ProtoLayoutComplicationData>() {
- /**
- * @param ambientLayout The [LayoutElementBuilders.Layout] to be displayed when the device
- * is ambient
- * @param interactiveLayout The [LayoutElementBuilders.Layout] to be displayed when the
- * device is interactive
- * @param resources The [ResourceBuilders.Resources] for [interactiveLayout] and
- * [ambientLayout]
- * @param contentDescription Localized description for use by screen readers
- */
- constructor(
- ambientLayout: LayoutElementBuilders.Layout,
- interactiveLayout: LayoutElementBuilders.Layout,
- resources: ResourceBuilders.Resources,
- contentDescription: ComplicationText
- ) : this(
- ambientLayout.toByteArray(),
- interactiveLayout.toByteArray(),
- resources.toByteArray(),
- contentDescription
- )
-
- private var tapAction: PendingIntent? = null
- private var validTimeRange: TimeRange? = null
-
- /** Sets optional pending intent to be invoked when the complication is tapped. */
- public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
- this.tapAction = tapAction
- }
-
- /** Sets optional time range during which the complication has to be shown. */
- @Suppress("MissingGetterMatchingBuilder") // b/174052810
- public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
- this.validTimeRange = validTimeRange
- }
-
- /** Builds the [ProtoLayoutComplicationData]. */
- public override fun build(): ProtoLayoutComplicationData =
- ProtoLayoutComplicationData(
- ambientLayout,
- interactiveLayout,
- layoutResources,
- contentDescription,
- tapAction,
- validTimeRange,
- cachedWireComplicationData,
- dataSource,
- persistencePolicy,
- displayPolicy
- )
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder()
- .apply { fillWireComplicationDataBuilder(this) }
- .build()
- .also { cachedWireComplicationData = it }
- }
-
- override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
- builder.setInteractiveLayout(interactiveLayoutWireFormat)
- builder.setAmbientLayout(ambientLayoutWireFormat)
- builder.setLayoutResources(layoutResourcesWireFormat)
- builder.setContentDescription(
- when (contentDescription) {
- ComplicationText.EMPTY -> null
- else -> contentDescription?.toWireComplicationText()
- }
- )
- builder.setTapAction(tapAction)
- setValidTimeRange(validTimeRange, builder)
- builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
- }
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as ProtoLayoutComplicationData
-
- if (!interactiveLayoutWireFormat.contentEquals(other.interactiveLayoutWireFormat))
- return false
- if (!ambientLayoutWireFormat.contentEquals(other.ambientLayoutWireFormat)) return false
- if (!layoutResourcesWireFormat.contentEquals(other.layoutResourcesWireFormat)) return false
- if (contentDescription != other.contentDescription) return false
- if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
- if (tapAction != other.tapAction) return false
- if (validTimeRange != other.validTimeRange) return false
- if (dataSource != other.dataSource) return false
- if (persistencePolicy != other.persistencePolicy) return false
- if (displayPolicy != other.displayPolicy) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = interactiveLayoutWireFormat.hashCode()
- result = 31 * result + ambientLayoutWireFormat.hashCode()
- result = 31 * result + layoutResourcesWireFormat.hashCode()
- result = 31 * result + (contentDescription?.hashCode() ?: 0)
- result = 31 * result + tapActionLostDueToSerialization.hashCode()
- result = 31 * result + (tapAction?.hashCode() ?: 0)
- result = 31 * result + validTimeRange.hashCode()
- result = 31 * result + dataSource.hashCode()
- result = 31 * result + persistencePolicy.hashCode()
- result = 31 * result + persistencePolicy.hashCode()
- result = 31 * result + displayPolicy.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ProtoLayoutComplicationData(protoLayoutWireFormat=$interactiveLayoutWireFormat, " +
- "ambientProtoLayoutWireFormat=$ambientLayoutWireFormat, " +
- "resourcesWireFormat=$layoutResourcesWireFormat, " +
- "contentDescription=$contentDescription, " +
- "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
- "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
- "persistencePolicy=$persistencePolicy, displayPolicy=$displayPolicy)"
- }
-
- /** @hide */
- public companion object {
- /** The [ComplicationType] corresponding to objects of this type. */
- @JvmField
- public val TYPE: ComplicationType = ComplicationType.PROTO_LAYOUT
- }
-}
-
-/**
- * A complication that's a list of other complications, typically rendered as a table. E.g. the
- * weather forecast for the next three days could consist of three [ShortTextComplicationData]s
- * displayed in a row of columns.
- *
- * @property complicationList The list of sub [ComplicationData]s to display. This has a maximum
- * size of [ListComplicationData.MAX_ITEMS]. Note complicationList may not include a
- * ListComplicationData.
- * @property contentDescription The content description field for accessibility and is used to
- * describe what data the image represents. If the image is purely stylistic, and does not convey
- * any information to the user, then provide an empty content description. If no content description
- * is provided, a generic content description will be used instead.
- * @property styleHint The [StyleHint] which influences layout.
- */
-@ComplicationExperimental
-public class ListComplicationData
-internal constructor(
- public val complicationList: List<ComplicationData>,
- public val contentDescription: ComplicationText?,
- tapAction: PendingIntent?,
- validTimeRange: TimeRange?,
- cachedWireComplicationData: WireComplicationData?,
- dataSource: ComponentName?,
- public val styleHint: StyleHint,
- @ComplicationPersistencePolicy persistencePolicy: Int,
- @ComplicationDisplayPolicy displayPolicy: Int
-) : ComplicationData(
- TYPE,
- tapAction = tapAction,
- cachedWireComplicationData = cachedWireComplicationData,
- validTimeRange = validTimeRange ?: TimeRange.ALWAYS,
- dataSource = dataSource,
- persistencePolicy = persistencePolicy,
- displayPolicy = displayPolicy
-) {
-
- init {
- require(complicationList.size <= MAX_ITEMS) {
- "complicationList has a maximum of $MAX_ITEMS entries, but found " +
- complicationList.size
- }
-
- for (entry in complicationList) {
- require(entry !is ListComplicationData) {
- "You may not include a ListComplicationData inside a ListComplicationData"
- }
- }
- }
-
- /** A hint for generating a layout for [ListComplicationData]. */
- @ComplicationExperimental
- enum class StyleHint(private val wireType: Int) {
- /** Hints the list should be displayed as a single column where the entries are rows. */
- ColumnOfRows(0),
-
- /** Hints the list should be displayed as a single row where the entries are columns. */
- RowOfColumns(1);
-
- override fun toString(): String {
- return "ListComplicationLayoutStyleHint(wireType=$wireType)"
- }
-
- internal companion object {
- fun fromWireFormat(wireType: Int): StyleHint =
- when (wireType) {
- ColumnOfRows.ordinal -> ColumnOfRows
- RowOfColumns.ordinal -> RowOfColumns
- else ->
- throw java.lang.IllegalArgumentException(
- "Unrecognized ListComplicationLayoutStyleHint wireType $wireType"
- )
- }
- }
- }
-
- /**
- * Builder for [ListComplicationData].
- *
- * You must at a minimum set the [complicationList], [styleHint] and [contentDescription]
- * fields.
- *
- * @param complicationList The list [ComplicationData] to be displayed, typically as a table.
- * Note complicationList may not include a ListComplicationData.
- * @param styleHint The [StyleHint] which influences layout.
- * @param contentDescription Localized description for use by screen readers
- */
- public class Builder(
- private val complicationList: List<ComplicationData>,
- private val styleHint: StyleHint,
- private val contentDescription: ComplicationText
- ) : BaseBuilder<Builder, ListComplicationData>() {
- private var tapAction: PendingIntent? = null
- private var validTimeRange: TimeRange? = null
-
- /** Sets optional pending intent to be invoked when the complication is tapped. */
- @SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
- public fun setTapAction(tapAction: PendingIntent?): Builder = apply {
- this.tapAction = tapAction
- }
-
- /** Sets optional time range during which the complication has to be shown. */
- @SuppressWarnings("MissingGetterMatchingBuilder") // See http://b/174052810
- public fun setValidTimeRange(validTimeRange: TimeRange?): Builder = apply {
- this.validTimeRange = validTimeRange
- }
-
- /** Builds the [ListComplicationData]. */
- public override fun build(): ListComplicationData =
- ListComplicationData(
- complicationList,
- contentDescription,
- tapAction,
- validTimeRange,
- cachedWireComplicationData,
- dataSource,
- styleHint,
- persistencePolicy,
- displayPolicy
- )
- }
-
- /** @hide */
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- override fun asWireComplicationData(): WireComplicationData {
- cachedWireComplicationData?.let {
- return it
- }
- return createWireComplicationDataBuilder()
- .apply { fillWireComplicationDataBuilder(this) }
- .build()
- .also { cachedWireComplicationData = it }
- }
-
- override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
- builder.setListEntryCollection(complicationList.map {
- it.asWireComplicationData()
- })
- builder.setListStyleHint(styleHint.ordinal)
- builder.setContentDescription(
- when (contentDescription) {
- ComplicationText.EMPTY -> null
- else -> contentDescription?.toWireComplicationText()
- }
- )
- builder.setTapAction(tapAction)
- setValidTimeRange(validTimeRange, builder)
- builder.setTapActionLostDueToSerialization(tapActionLostDueToSerialization)
- }
-
- override fun hasPlaceholderFields() = false
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as ListComplicationData
-
- if (complicationList != other.complicationList) return false
- if (contentDescription != other.contentDescription) return false
- if (tapActionLostDueToSerialization != other.tapActionLostDueToSerialization) return false
- if (tapAction != other.tapAction) return false
- if (validTimeRange != other.validTimeRange) return false
- if (dataSource != other.dataSource) return false
- if (styleHint != other.styleHint) return false
- if (persistencePolicy != other.persistencePolicy) return false
- if (displayPolicy != other.displayPolicy) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = complicationList.hashCode()
- result = 31 * result + (contentDescription?.hashCode() ?: 0)
- result = 31 * result + tapActionLostDueToSerialization.hashCode()
- result = 31 * result + (tapAction?.hashCode() ?: 0)
- result = 31 * result + validTimeRange.hashCode()
- result = 31 * result + dataSource.hashCode()
- result = 31 * result + styleHint.hashCode()
- result = 31 * result + persistencePolicy.hashCode()
- result = 31 * result + displayPolicy.hashCode()
- return result
- }
-
- override fun toString(): String {
- return "ListComplicationData(complicationList=$complicationList, " +
- "contentDescription=$contentDescription, " +
- "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
- "tapAction=$tapAction, validTimeRange=$validTimeRange, dataSource=$dataSource, " +
- "styleHint=$styleHint, persistencePolicy=$persistencePolicy, " +
- "displayPolicy=$displayPolicy)"
- }
-
- /** @hide */
- public companion object {
- /** The [ComplicationType] corresponding to objects of this type. */
- @JvmField
- public val TYPE: ComplicationType = ComplicationType.LIST
-
- /** The maximum number of items in [ListComplicationData.complicationList]. */
- public const val MAX_ITEMS = 5
- }
-}
-
-/**
* Type sent by the system when the watch face does not have permission to receive complication
* data.
*
@@ -2948,7 +2503,6 @@
}
}
-@OptIn(ComplicationExperimental::class)
@Suppress("NewApi")
internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? {
try {
@@ -3049,38 +2603,6 @@
displayPolicyCopy
)
- // TODO(b/230102159): We need to build support for placeholder ProtoLayoutComplicationData.
- ProtoLayoutComplicationData.TYPE.toWireComplicationType() ->
- ProtoLayoutComplicationData.Builder(
- ambientLayout!!,
- interactiveLayout!!,
- layoutResources!!,
- contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
- )
- .apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }
- .build()
-
- ListComplicationData.TYPE.toWireComplicationType() ->
- ListComplicationData.Builder(
- listEntries!!.map { it.toApiComplicationData() },
- ListComplicationData.StyleHint.fromWireFormat(listStyleHint),
- contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
- )
- .apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }
- .build()
-
GoalProgressComplicationData.TYPE.toWireComplicationType() ->
GoalProgressComplicationData.Builder(
value = rangedValue,
@@ -3145,7 +2667,6 @@
/**
* @hide
*/
-@OptIn(ComplicationExperimental::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@Suppress("NewApi")
public fun WireComplicationData.toApiComplicationData(): ComplicationData {
@@ -3261,39 +2782,6 @@
setDisplayPolicy(displayPolicyCopy)
}.build()
- ProtoLayoutComplicationData.TYPE.toWireComplicationType() ->
- ProtoLayoutComplicationData.Builder(
- ambientLayout!!,
- interactiveLayout!!,
- layoutResources!!,
- contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
- )
- .apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }
- .build()
-
- ListComplicationData.TYPE.toWireComplicationType() ->
- ListComplicationData.Builder(
- listEntries!!.map { it.toApiComplicationData() },
- ListComplicationData.StyleHint.fromWireFormat(listStyleHint),
- contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
- )
- .apply {
- setTapAction(tapAction)
- setValidTimeRange(parseTimeRange())
- setCachedWireComplicationData(wireComplicationData)
- setDataSource(dataSourceCopy)
- setPersistencePolicy(persistencePolicyCopy)
- setDisplayPolicy(displayPolicyCopy)
- }
- .build()
-
NoPermissionComplicationData.TYPE.toWireComplicationType() ->
NoPermissionComplicationData.Builder().apply {
setMonochromaticImage(parseIcon())
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
index 2d02345..49b6a23 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Type.kt
@@ -16,6 +16,8 @@
package androidx.wear.watchface.complications.data
+import android.os.Build
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
/**
@@ -36,15 +38,10 @@
SMALL_IMAGE(WireComplicationData.TYPE_SMALL_IMAGE),
PHOTO_IMAGE(WireComplicationData.TYPE_LARGE_IMAGE),
NO_PERMISSION(WireComplicationData.TYPE_NO_PERMISSION),
-
- @ComplicationExperimental
- PROTO_LAYOUT(WireComplicationData.EXP_TYPE_PROTO_LAYOUT),
- @ComplicationExperimental
- GOAL_PROGRESS(WireComplicationData.EXP_TYPE_GOAL_PROGRESS),
- @ComplicationExperimental
- WEIGHTED_ELEMENTS(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS),
- @ComplicationExperimental
- LIST(WireComplicationData.EXP_TYPE_LIST);
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ GOAL_PROGRESS(WireComplicationData.TYPE_GOAL_PROGRESS),
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+ WEIGHTED_ELEMENTS(WireComplicationData.TYPE_WEIGHTED_ELEMENTS);
/**
* Converts this value to the integer value used for serialization.
@@ -69,6 +66,7 @@
@OptIn(ComplicationExperimental::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
+ @Suppress("NewApi")
public fun fromWireType(wireType: Int): ComplicationType =
when (wireType) {
NO_DATA.wireType -> NO_DATA
@@ -81,10 +79,8 @@
SMALL_IMAGE.wireType -> SMALL_IMAGE
PHOTO_IMAGE.wireType -> PHOTO_IMAGE
NO_PERMISSION.wireType -> NO_PERMISSION
- PROTO_LAYOUT.wireType -> PROTO_LAYOUT
GOAL_PROGRESS.wireType -> GOAL_PROGRESS
WEIGHTED_ELEMENTS.wireType -> WEIGHTED_ELEMENTS
- LIST.wireType -> LIST
else -> EMPTY
}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 340b6ee..a369ee0 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -398,7 +398,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData() {
val data = RangedValueComplicationData.Builder(
@@ -467,7 +466,6 @@
}
@RequiresApi(Build.VERSION_CODES.P)
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData_withImages() {
val data = RangedValueComplicationData.Builder(
@@ -551,7 +549,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData_with_ColorRamp() {
val data = GoalProgressComplicationData.Builder(
@@ -564,7 +561,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -622,7 +619,6 @@
}
@RequiresApi(Build.VERSION_CODES.P)
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData_with_ColorRamp_and_Images() {
val data = GoalProgressComplicationData.Builder(
@@ -637,7 +633,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setIcon(monochromaticImageIcon)
@@ -706,7 +702,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData_with_ColorRamp() {
val data = RangedValueComplicationData.Builder(
@@ -779,7 +774,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData() {
val data = WeightedElementsComplicationData.Builder(
@@ -796,7 +790,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.GRAY)
@@ -866,7 +860,6 @@
}
@RequiresApi(Build.VERSION_CODES.P)
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData_withImages() {
val data = WeightedElementsComplicationData.Builder(
@@ -884,7 +877,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -1239,139 +1232,6 @@
}
@Test
- @OptIn(ComplicationExperimental::class)
- public fun listComplicationData() {
- val data = ListComplicationData.Builder(
- listOf(
- ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- )
- .setDataSource(dataSourceA).build()
-
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
- .setListEntryCollection(
- listOf(
- WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
- .setPersistencePolicy(
- ComplicationPersistencePolicies.CACHING_ALLOWED
- )
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedValueType(RangedValueTypes.UNDEFINED)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortText(WireComplicationText.plainText("battery"))
- .setPersistencePolicy(
- ComplicationPersistencePolicies.CACHING_ALLOWED
- )
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- )
- .setListStyleHint(ListComplicationData.StyleHint.RowOfColumns.ordinal)
- .setDataSource(dataSourceA)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- testRoundTripConversions(data)
- val deserialized = serializeAndDeserialize(data) as ListComplicationData
- assertThat(deserialized.complicationList).containsExactly(
- ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- )
- assertThat(deserialized.styleHint).isEqualTo(ListComplicationData.StyleHint.RowOfColumns)
-
- val data2 = ListComplicationData.Builder(
- listOf(
- ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- ).setDataSource(dataSourceA).build()
-
- val data3 = ListComplicationData.Builder(
- listOf(
- ShortTextComplicationData.Builder("text2".complicationText, ComplicationText.EMPTY)
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- ).setDataSource(dataSourceB).build()
-
- assertThat(data).isEqualTo(data2)
- assertThat(data).isNotEqualTo(data3)
- assertThat(data.hashCode()).isEqualTo(data2.hashCode())
- assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
- assertThat(data.toString()).isEqualTo(
- "ListComplicationData(complicationList=[ShortTextComplicationData(" +
- "text=ComplicationText{mSurroundingText=text, mTimeDependentText=null}, " +
- "title=null, monochromaticImage=null, smallImage=null, " +
- "contentDescription=ComplicationText{mSurroundingText=, mTimeDependentText=null}," +
- " tapActionLostDueToSerialization=false, tapAction=null, " +
- "validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
- "persistencePolicy=0, displayPolicy=0), RangedValueComplicationData(value=95.0, " +
- "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
- "title=null, text=ComplicationText{mSurroundingText=battery, " +
- "mTimeDependentText=null}, contentDescription=ComplicationText{mSurroundingText=," +
- " mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
- "tapAction=null, validTimeRange=TimeRange(" +
- "startDateTimeMillis=-1000000000-01-01T00:00:00Z, " +
- "endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=null, " +
- "colorRamp=null, persistencePolicy=0, displayPolicy=0)], " +
- "contentDescription=ComplicationText{mSurroundingText=, mTimeDependentText=null}," +
- " tapActionLostDueToSerialization=false, tapAction=null," +
- " validTimeRange=TimeRange(startDateTimeMillis=-1000000000-01-01T00:00:00Z," +
- " endDateTimeMillis=+1000000000-12-31T23:59:59.999999999Z), dataSource=" +
- "ComponentInfo{com.pkg_a/com.a}, " +
- "styleHint=ListComplicationLayoutStyleHint(wireType=1), persistencePolicy=0, " +
- "displayPolicy=0)"
- )
- }
-
- @Test
public fun noDataComplicationData_shortText() {
val data = NoDataComplicationData(
ShortTextComplicationData.Builder(
@@ -1513,7 +1373,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_rangedValue() {
val data = NoDataComplicationData(
@@ -1598,7 +1457,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_goalProgress() {
val data = NoDataComplicationData(
@@ -1616,7 +1474,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(GoalProgressComplicationData.PLACEHOLDER)
.setTargetValue(10000f)
.setShortText(ComplicationText.PLACEHOLDER.toWireComplicationText())
@@ -1684,7 +1542,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_weightedElements() {
val data = NoDataComplicationData(
@@ -1705,7 +1562,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.GRAY)
@@ -1777,7 +1634,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_rangedValue_with_ColorRange() {
val data = NoDataComplicationData(
@@ -2147,7 +2003,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData() {
assertRoundtrip(
@@ -2164,7 +2019,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData_drawSegmented() {
assertRoundtrip(
@@ -2181,11 +2035,10 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -2199,11 +2052,10 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData() {
assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.DKGRAY)
@@ -2272,52 +2124,6 @@
}
@Test
- @OptIn(ComplicationExperimental::class)
- public fun protoLayoutComplicationData() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
-
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
- .setAmbientLayout(ambientLayout)
- .setInteractiveLayout(interactiveLayout)
- .setLayoutResources(resources)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.PROTO_LAYOUT
- )
- }
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun listComplicationData() {
- val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
- .build()
-
- val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortText(WireComplicationText.plainText("battery"))
- .build()
-
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
- .setListEntryCollection(listOf(wireShortText, wireRangedValue))
- .setListStyleHint(
- ListComplicationData.StyleHint.RowOfColumns.ordinal
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.LIST
- )
- }
-
- @Test
public fun noDataComplicationData_shortText() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
@@ -2365,7 +2171,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_rangedValue() {
val icon = Icon.createWithContentUri("someuri")
@@ -2393,14 +2198,13 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_goalProgress() {
val icon = Icon.createWithContentUri("someuri")
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -2421,13 +2225,12 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_weightedElementsComplicationData() {
assertRoundtrip(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.DKGRAY)
@@ -2491,64 +2294,6 @@
)
}
- @Test
- public fun noDataComplicationData_protoLayout() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
- .setAmbientLayout(ambientLayout)
- .setInteractiveLayout(interactiveLayout)
- .setLayoutResources(resources)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.NO_DATA
- )
- }
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun noDataComplicationData_list() {
- val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
- .build()
-
- val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortTitle(WireComplicationText.plainText("battery"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
-
- assertRoundtrip(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
- .setListEntryCollection(listOf(wireShortText, wireRangedValue))
- .setListStyleHint(
- ListComplicationData.StyleHint.RowOfColumns.ordinal
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build(),
- ComplicationType.NO_DATA
- )
- }
-
private fun assertRoundtrip(wireData: WireComplicationData, type: ComplicationType) {
val data = wireData.toApiComplicationData()
assertThat(data.type).isEqualTo(type)
@@ -2596,10 +2341,10 @@
).isEqualTo(mPendingIntent)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData() {
assertThat(
+ @Suppress("NewApi")
GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
contentDescription = "content description".complicationText
@@ -2611,10 +2356,10 @@
).isEqualTo(mPendingIntent)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData() {
assertThat(
+ @Suppress("NewApi")
WeightedElementsComplicationData.Builder(
listOf(
WeightedElementsComplicationData.Element(0.5f, Color.RED),
@@ -2676,44 +2421,6 @@
).asWireComplicationData().placeholder?.tapAction
).isEqualTo(mPendingIntent)
}
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun protoLayoutComplicationData() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
-
- assertThat(
- ProtoLayoutComplicationData.Builder(
- ambientLayout,
- interactiveLayout,
- resources,
- ComplicationText.EMPTY
- )
- .setTapAction(mPendingIntent)
- .build()
- .tapAction
- ).isEqualTo(mPendingIntent)
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
- public fun listComplicationData() {
- val icon = Icon.createWithContentUri("someuri")
- val image = SmallImage.Builder(icon, SmallImageType.PHOTO).build()
- assertThat(
- ListComplicationData.Builder(
- listOf(
- SmallImageComplicationData.Builder(image, ComplicationText.EMPTY).build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- )
- .setTapAction(mPendingIntent).build()
- .tapAction
- ).isEqualTo(mPendingIntent)
- }
}
@RunWith(SharedRobolectricTestRunner::class)
@@ -2756,10 +2463,10 @@
).isEqualTo(mPendingIntent)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData() {
assertThat(
+ @Suppress("NewApi")
GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
contentDescription = "content description".complicationText
@@ -2771,10 +2478,10 @@
).isEqualTo(mPendingIntent)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData() {
assertThat(
+ @Suppress("NewApi")
WeightedElementsComplicationData.Builder(
listOf(
WeightedElementsComplicationData.Element(0.5f, Color.RED),
@@ -2839,46 +2546,6 @@
).asWireComplicationData().toApiComplicationData().tapAction
).isEqualTo(mPendingIntent)
}
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun protoLayoutComplicationData() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
-
- assertThat(
- ProtoLayoutComplicationData.Builder(
- ambientLayout,
- interactiveLayout,
- resources,
- ComplicationText.EMPTY
- )
- .setTapAction(mPendingIntent)
- .build()
- .asWireComplicationData().toApiComplicationData().tapAction
- ).isEqualTo(mPendingIntent)
- }
-
- @OptIn(ComplicationExperimental::class)
- @Test
- public fun listComplicationData() {
- val icon = Icon.createWithContentUri("someuri")
- val image = SmallImage.Builder(icon, SmallImageType.PHOTO).build()
- assertThat(
- ListComplicationData.Builder(
- listOf(
- SmallImageComplicationData.Builder(image, ComplicationText.EMPTY).build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- )
- .setTapAction(mPendingIntent).build()
- .asWireComplicationData()
- .toApiComplicationData()
- .tapAction
- ).isEqualTo(mPendingIntent)
- }
}
@RunWith(SharedRobolectricTestRunner::class)
@@ -2923,7 +2590,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun rangedValueComplicationData() {
val data = RangedValueComplicationData.Builder(
@@ -2949,7 +2615,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun goalProgressComplicationData() {
val data = GoalProgressComplicationData.Builder(
@@ -2962,7 +2627,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -2977,7 +2642,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun weightedElementsComplicationData() {
val data = WeightedElementsComplicationData.Builder(
@@ -2993,7 +2657,7 @@
.build()
ParcelableSubject.assertThat(data.asWireComplicationData())
.hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -3065,92 +2729,6 @@
}
@Test
- @OptIn(ComplicationExperimental::class)
- public fun protoLayout() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
-
- val data = ProtoLayoutComplicationData.Builder(
- ambientLayout,
- interactiveLayout,
- resources,
- ComplicationText.EMPTY
- )
- .setValidTimeRange(TimeRange.between(testStartInstant, testEndDateInstant))
- .build()
-
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
- .setAmbientLayout(ambientLayout)
- .setInteractiveLayout(interactiveLayout)
- .setLayoutResources(resources)
- .setStartDateTimeMillis(testStartInstant.toEpochMilli())
- .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun list() {
- val data = ListComplicationData.Builder(
- listOf(
- ShortTextComplicationData.Builder(
- "text".complicationText,
- ComplicationText.EMPTY
- )
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- )
- .setValidTimeRange(TimeRange.between(testStartInstant, testEndDateInstant))
- .build()
-
- val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
-
- val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedValueType(RangedValueTypes.UNDEFINED)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortText(WireComplicationText.plainText("battery"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
-
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
- .setListEntryCollection(listOf(wireShortText, wireRangedValue))
- .setListStyleHint(
- ListComplicationData.StyleHint.RowOfColumns.ordinal
- )
- .setStartDateTimeMillis(testStartInstant.toEpochMilli())
- .setEndDateTimeMillis(testEndDateInstant.toEpochMilli())
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
-
- @Test
public fun noDataComplicationData_shortText() {
val data = NoDataComplicationData(
ShortTextComplicationData.Builder("text".complicationText, ComplicationText.EMPTY)
@@ -3194,7 +2772,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_rangedValue() {
val data = NoDataComplicationData(
@@ -3227,7 +2804,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_goalProgress() {
val data = NoDataComplicationData(
@@ -3243,7 +2819,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_GOAL_PROGRESS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
.setRangedValue(1200f)
.setTargetValue(10000f)
.setShortTitle(WireComplicationText.plainText("steps"))
@@ -3262,7 +2838,6 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
public fun noDataComplicationData_weightedElements() {
val data = NoDataComplicationData(
@@ -3281,7 +2856,7 @@
.hasSameSerializationAs(
WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
.setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_WEIGHTED_ELEMENTS)
+ WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
.setElementWeights(floatArrayOf(0.5f, 1f, 2f))
.setElementColors(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
.setElementBackgroundColor(Color.TRANSPARENT)
@@ -3367,99 +2942,6 @@
.build()
)
}
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun noDataComplicationData_protoLayout() {
- val ambientLayout = ByteArray(1)
- val interactiveLayout = ByteArray(2)
- val resources = ByteArray(3)
-
- val data = NoDataComplicationData(
- ProtoLayoutComplicationData.Builder(
- ambientLayout,
- interactiveLayout,
- resources,
- ComplicationText.EMPTY
- ).build()
- )
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_PROTO_LAYOUT)
- .setAmbientLayout(ambientLayout)
- .setInteractiveLayout(interactiveLayout)
- .setLayoutResources(resources)
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
-
- @Test
- @OptIn(ComplicationExperimental::class)
- public fun noDataComplicationData_list() {
- val data = NoDataComplicationData(
- ListComplicationData.Builder(
- listOf(
- ShortTextComplicationData.Builder(
- "text".complicationText,
- ComplicationText.EMPTY
- )
- .build(),
- RangedValueComplicationData.Builder(
- value = 95f,
- min = 0f,
- max = 100f,
- ComplicationText.EMPTY
- )
- .setText("battery".complicationText)
- .build()
- ),
- ListComplicationData.StyleHint.RowOfColumns,
- ComplicationText.EMPTY
- ).build()
- )
-
- val wireShortText = WireComplicationDataBuilder(WireComplicationData.TYPE_SHORT_TEXT)
- .setShortText(WireComplicationText.plainText("text"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
-
- val wireRangedValue = WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
- .setRangedValue(95f)
- .setRangedValueType(RangedValueTypes.UNDEFINED)
- .setRangedMinValue(0f)
- .setRangedMaxValue(100f)
- .setShortText(WireComplicationText.plainText("battery"))
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
-
- ParcelableSubject.assertThat(data.asWireComplicationData())
- .hasSameSerializationAs(
- WireComplicationDataBuilder(WireComplicationData.TYPE_NO_DATA)
- .setPlaceholder(
- WireComplicationDataBuilder(WireComplicationData.EXP_TYPE_LIST)
- .setListEntryCollection(listOf(wireShortText, wireRangedValue))
- .setListStyleHint(
- ListComplicationData.StyleHint.RowOfColumns.ordinal
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
- .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
- .build()
- )
- }
}
@RunWith(SharedRobolectricTestRunner::class)
@@ -3549,8 +3031,8 @@
)
}
- @OptIn(ComplicationExperimental::class)
@Test
+ @Suppress("NewApi")
fun goalProgress() {
val data = GoalProgressComplicationData.Builder(
value = 1200f, targetValue = 10000f,
@@ -3571,7 +3053,7 @@
"persistencePolicy=0, displayPolicy=0)"
)
assertThat(data.asWireComplicationData().toString()).isEqualTo(
- "ComplicationData{mType=-13, mFields=REDACTED}"
+ "ComplicationData{mType=13, mFields=REDACTED}"
)
}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
index dbedd54..c70e7a6 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/PlaceholderTest.kt
@@ -29,6 +29,7 @@
import org.junit.runner.RunWith
@RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
class PlaceholderTest {
val text = "text".complicationText
val title = "title".complicationText
diff --git a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
index 96524a6..8ee1c50 100644
--- a/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
+++ b/wear/watchface/watchface-complications-rendering/src/main/java/androidx/wear/watchface/complications/rendering/ComplicationRenderer.java
@@ -16,6 +16,12 @@
package androidx.wear.watchface.complications.rendering;
+import static androidx.core.util.Preconditions.checkNotNull;
+
+import static java.lang.Math.cos;
+import static java.lang.Math.sin;
+import static java.lang.Math.toRadians;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
@@ -24,12 +30,14 @@
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.DashPathEffect;
+import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
+import android.graphics.SweepGradient;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.Icon.OnDrawableLoadedListener;
@@ -46,6 +54,7 @@
import androidx.annotation.VisibleForTesting;
import androidx.wear.watchface.complications.data.ImageKt;
import androidx.wear.watchface.complications.data.RangedValueComplicationData;
+import androidx.wear.watchface.complications.data.RangedValueTypes;
import androidx.wear.watchface.complications.rendering.utils.IconLayoutHelper;
import androidx.wear.watchface.complications.rendering.utils.LargeImageLayoutHelper;
import androidx.wear.watchface.complications.rendering.utils.LayoutHelper;
@@ -84,6 +93,9 @@
@VisibleForTesting
static final int STROKE_GAP_IN_DEGREES = 4;
+ /** The gap between in progress stroke for scores. */
+ static final int STROKE_GAP_IN_DEGREES_FOR_SCORE = 15;
+
/**
* Starting angle for ranged value, i.e. in progress part will start from this angle. As it's
* drawn clockwise, -90 corresponds to 12 o'clock on a watch.
@@ -110,6 +122,18 @@
@VisibleForTesting
static final Paint PLACEHOLDER_PAINT = createPlaceHolderPaint();
+ /** Defines the placeholder shape for WeightedElementsComplicationData. */
+ private static final float[] PLACEHOLDER_WEIGHTS = {3.0f, 2.0f, 1.0f};
+
+ /** Defines the gap between weighted elements in degrees. */
+ private static final float WEIGHTED_ANGLE_GAP = 15.0f;
+
+ /** Used for goal progress to denote over achievement */
+ private static final float OVER_ACHIEVEMENT_ARC_LENGTH = 36.0f;
+
+ /** The fraction of the progress bar reserved for progress beyond the target. */
+ private static final float OVER_ACHIEVEMENT_FRACTION = 1.1f;
+
private static Paint createPlaceHolderPaint() {
Paint paint = new Paint();
paint.setColor(Color.LTGRAY);
@@ -167,6 +191,8 @@
@VisibleForTesting
boolean mIsPlaceholderRangedValue;
@VisibleForTesting
+ boolean mIsPlaceholderWeightedElements;
+ @VisibleForTesting
boolean mIsPlaceholderTitle;
@VisibleForTesting
boolean mIsPlaceholderText;
@@ -283,6 +309,7 @@
mIsPlaceholderSmallImage = false;
mIsPlaceholderLargeImage = false;
mIsPlaceholderRangedValue = false;
+ mIsPlaceholderWeightedElements = false;
mIsPlaceholderTitle = false;
mIsPlaceholderText = false;
mIsPlaceholder = false;
@@ -297,8 +324,9 @@
mIsPlaceholderLargeImage =
data.hasLargeImage() && ImageKt.isPlaceholder(data.getLargeImage());
mIsPlaceholderRangedValue = data.hasRangedValue()
- && data.getRangedValue()
- == RangedValueComplicationData.PLACEHOLDER;
+ && data.getRangedValue() == RangedValueComplicationData.PLACEHOLDER;
+ mIsPlaceholderWeightedElements = data.getElementWeights() != null
+ && data.getElementWeights().length == 0;
if (data.getType() == ComplicationData.TYPE_LONG_TEXT) {
mIsPlaceholderTitle =
data.hasLongTitle() && data.getLongTitle().isPlaceholder();
@@ -450,6 +478,7 @@
drawSmallImage(canvas, currentPaintSet, mIsPlaceholderSmallImage);
drawLargeImage(canvas, currentPaintSet, mIsPlaceholderLargeImage);
drawRangedValue(canvas, currentPaintSet, mIsPlaceholderRangedValue);
+ drawWeightedElements(canvas, currentPaintSet, mIsPlaceholderWeightedElements);
drawMainText(canvas, currentPaintSet, mIsPlaceholderText);
drawSubText(canvas, currentPaintSet, mIsPlaceholderTitle);
// Draw highlight if highlighted
@@ -602,6 +631,11 @@
canvas.drawRect(mRangedValueBoundsF, mDebugPaint);
}
+ if (mComplicationData.getType() == ComplicationData.TYPE_GOAL_PROGRESS) {
+ drawGoalProgress(canvas, paintSet, isPlaceholder);
+ return;
+ }
+
float rangedMinValue = mComplicationData.getRangedMinValue();
float rangedMaxValue = mComplicationData.getRangedMaxValue();
float rangedValue = mComplicationData.getRangedValue();
@@ -616,9 +650,17 @@
Math.min(rangedMaxValue, Math.max(rangedMinValue, rangedValue)) - rangedMinValue;
float interval = rangedMaxValue - rangedMinValue;
float progress = interval > 0 ? value / interval : 0;
+ int valueType = mComplicationData.getRangedValueType();
- // do not need to draw gap in the cases of full circle
- float gap = (progress > 0.0f && progress < 1.0f) ? STROKE_GAP_IN_DEGREES : 0.0f;
+ float gap;
+ if (valueType == RangedValueTypes.SCORE) {
+ gap = STROKE_GAP_IN_DEGREES_FOR_SCORE;
+ } else if (progress <= 0 || progress >= 1.0f) {
+ // We do not need to draw a gap when there's either 0% or 100% progress.
+ gap = 0.0f;
+ } else {
+ gap = STROKE_GAP_IN_DEGREES;
+ }
float inProgressAngle = Math.max(0, 360.0f * progress - gap);
float remainderAngle = Math.max(0, 360.0f * (1.0f - progress) - gap);
@@ -629,26 +671,203 @@
PLACEHOLDER_PROGRESS_PAINT.setStrokeWidth(paintSet.mInProgressPaint.getStrokeWidth());
}
- // Draw the progress arc.
+ float startAngle = RANGED_VALUE_START_ANGLE + gap / 2.0f;
+ switch (valueType) {
+ case RangedValueTypes.SCORE: {
+ float sweepAngle = 360.0f - gap / 2;
+ drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle, sweepAngle);
+
+ // Draw the progress indicator.
+ float strokeWidth = paintSet.mInProgressPaint.getStrokeWidth();
+ float radiusX = mRangedValueBoundsF.width() * 0.5f;
+ float radiusY = mRangedValueBoundsF.height() * 0.5f;
+ float x = mRangedValueBoundsF.centerX()
+ + radiusX * (float) cos(toRadians(startAngle + inProgressAngle));
+ float y = mRangedValueBoundsF.centerY()
+ + radiusY * (float) sin(toRadians(startAngle + inProgressAngle));
+ canvas.drawCircle(x, y, strokeWidth,
+ isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+ break;
+ }
+
+ default:
+ case RangedValueTypes.UNDEFINED:
+ // Draw the arc represent by the value.
+ drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle, inProgressAngle);
+
+ // Draw an arc representing the remainder.
+ if (!isPlaceholder) {
+ canvas.drawArc(
+ mRangedValueBoundsF,
+ startAngle + inProgressAngle + gap,
+ remainderAngle,
+ /* useCenter = */ false,
+ paintSet.mRemainingPaint);
+ }
+ break;
+ }
+
+ mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
+ }
+
+ private void drawGoalProgress(Canvas canvas, PaintSet paintSet, boolean isPlaceholder) {
+ float rangedMaxValue = mComplicationData.getTargetValue() * OVER_ACHIEVEMENT_FRACTION;
+ float rangedValue = mComplicationData.getRangedValue();
+
+ if (rangedValue > rangedMaxValue) {
+ rangedValue = rangedMaxValue;
+ }
+
+ if (isPlaceholder) {
+ rangedMaxValue = 100.0f;
+ rangedValue = 75.0f;
+ }
+
+ float value = Math.min(rangedMaxValue, Math.max(0f, rangedValue));
+ float interval = rangedMaxValue;
+ float progress = interval > 0 ? value / interval : 0;
+ float gap = STROKE_GAP_IN_DEGREES_FOR_SCORE;
+ float inProgressAngle = Math.max(0, 360.0f * progress - gap);
+
+ int insetAmount = (int) Math.ceil(paintSet.mInProgressPaint.getStrokeWidth());
+ mRangedValueBoundsF.inset(insetAmount, insetAmount);
+
+ if (isPlaceholder) {
+ PLACEHOLDER_PROGRESS_PAINT.setStrokeWidth(paintSet.mInProgressPaint.getStrokeWidth());
+ }
+
+ float startAngle = RANGED_VALUE_START_ANGLE + gap / 2.0f;
+ float sweepAngle = 360.0f - gap / 2;
+
+ // Draw the fixed length progress arc.
+ drawProgressBarArc(canvas, isPlaceholder, paintSet, startAngle,
+ sweepAngle - OVER_ACHIEVEMENT_ARC_LENGTH);
+
+ // Draw the fixed length over-achievement achievement arc, resenting progress past the
+ // target.
+ int prevColor = paintSet.mInProgressPaint.getColor();
+ paintSet.mInProgressPaint.setColor(Color.RED);
canvas.drawArc(
mRangedValueBoundsF,
- RANGED_VALUE_START_ANGLE + gap / 2,
- inProgressAngle,
- false,
+ startAngle + sweepAngle - OVER_ACHIEVEMENT_ARC_LENGTH,
+ OVER_ACHIEVEMENT_ARC_LENGTH,
+ /* useCenter = */ false,
+ paintSet.mInProgressPaint);
+
+ paintSet.mInProgressPaint.setColor(prevColor);
+
+ // Draw the progress indicator circle.
+ float strokeWidth = paintSet.mInProgressPaint.getStrokeWidth();
+ float radiusX = mRangedValueBoundsF.width() * 0.5f;
+ float radiusY = mRangedValueBoundsF.height() * 0.5f;
+ float x = mRangedValueBoundsF.centerX()
+ + radiusX * (float) cos(toRadians(startAngle + inProgressAngle));
+ float y = mRangedValueBoundsF.centerY()
+ + radiusY * (float) sin(toRadians(startAngle + inProgressAngle));
+ canvas.drawCircle(x, y, strokeWidth,
isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
- // Draw the remain arc.
- if (!isPlaceholder) {
+ mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
+ }
+
+ private void drawProgressBarArc(Canvas canvas, boolean isPlaceholder, PaintSet paintSet,
+ float startAngle, float sweepAngle) {
+ int[] colorRamp = mComplicationData.getColorRamp();
+ if (colorRamp != null) {
+ if (!checkNotNull(mComplicationData.isColorRampInterpolated())) {
+ drawNonInterpolatedColorRampArc(
+ canvas, isPlaceholder, paintSet, startAngle, sweepAngle, colorRamp);
+ return;
+ }
+
+ // Set up the SweepGradient shader, rotated so the start is at the top (12 o'clock).
+ SweepGradient gradient =
+ new SweepGradient(mRangedValueBoundsF.centerX(), mRangedValueBoundsF.centerY(),
+ colorRamp, /* positions= */ null);
+ Matrix matrix = new Matrix();
+ matrix.postRotate(startAngle,
+ mRangedValueBoundsF.centerX(), mRangedValueBoundsF.centerY());
+ gradient.setLocalMatrix(matrix);
+ paintSet.mInProgressPaint.setShader(gradient);
+ }
+ canvas.drawArc(
+ mRangedValueBoundsF,
+ startAngle,
+ sweepAngle,
+ false,
+ isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+ paintSet.mInProgressPaint.setShader(null);
+ }
+
+ private void drawNonInterpolatedColorRampArc(
+ Canvas canvas, boolean isPlaceholder, PaintSet paintSet,
+ float startAngle, float sweepAngle, int[] colorRamp) {
+ // We need to draw the arc in segments of equal color.
+ float segmentSweepAngle = sweepAngle / (float) colorRamp.length;
+ int prevColor = paintSet.mInProgressPaint.getColor();
+ paintSet.mInProgressPaint.setColor(Color.RED);
+ for (int j : colorRamp) {
+ paintSet.mInProgressPaint.setColor(j);
canvas.drawArc(
mRangedValueBoundsF,
- RANGED_VALUE_START_ANGLE
- + gap / 2.0f
- + inProgressAngle
- + gap,
- remainderAngle,
+ startAngle,
+ segmentSweepAngle,
false,
- paintSet.mRemainingPaint);
+ isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+ startAngle += segmentSweepAngle;
}
+ paintSet.mInProgressPaint.setColor(prevColor);
+ }
+
+ private void drawWeightedElements(Canvas canvas, PaintSet paintSet, boolean isPlaceholder) {
+ if (mRangedValueBoundsF.isEmpty()
+ || mComplicationData.getType() != ComplicationData.TYPE_WEIGHTED_ELEMENTS) {
+ return;
+ }
+ if (DEBUG_MODE) {
+ canvas.drawRect(mRangedValueBoundsF, mDebugPaint);
+ }
+
+ int insetAmount = (int) Math.ceil(paintSet.mInProgressPaint.getStrokeWidth());
+ mRangedValueBoundsF.inset(insetAmount, insetAmount);
+
+ // Fill with the background color to show between elements.
+ if (!isPlaceholder) {
+ paintSet.mInProgressPaint.setColor(mComplicationData.getElementBackgroundColor());
+ canvas.drawArc(
+ mRangedValueBoundsF,
+ /* startAngle = */ 0f,
+ /* sweepAngle = */ 360.0f,
+ /* useCenter = */ false,
+ paintSet.mInProgressPaint);
+ }
+
+ float[] weights = isPlaceholder ? PLACEHOLDER_WEIGHTS :
+ mComplicationData.getElementWeights();
+ int[] colors = mComplicationData.getElementColors();
+ float sum = 0;
+ for (float weight : weights) {
+ sum += weight;
+ }
+
+ // Only add gaps between elements if we have more than one value.
+ float gapAngle = (weights.length > 1) ? WEIGHTED_ANGLE_GAP * (float) weights.length : 0f;
+ float scale = (360.0f - gapAngle) / sum;
+
+ // Draw each element.
+ float angle = RANGED_VALUE_START_ANGLE;
+ for (int i = 0; i < weights.length; i++) {
+ float sweepLength = weights[i] * scale;
+ paintSet.mInProgressPaint.setColor(colors[i]);
+ canvas.drawArc(
+ mRangedValueBoundsF,
+ angle,
+ sweepLength,
+ /* useCenter = */ false,
+ isPlaceholder ? PLACEHOLDER_PROGRESS_PAINT : paintSet.mInProgressPaint);
+ angle += sweepLength + WEIGHTED_ANGLE_GAP;
+ }
+
mRangedValueBoundsF.inset(-insetAmount, -insetAmount);
}
@@ -790,7 +1009,9 @@
case ComplicationData.TYPE_LONG_TEXT:
currentLayoutHelper = new LongTextLayoutHelper();
break;
+ case ComplicationData.TYPE_GOAL_PROGRESS:
case ComplicationData.TYPE_RANGED_VALUE:
+ case ComplicationData.TYPE_WEIGHTED_ELEMENTS:
if (mRangedValueProgressHidden) {
if (mComplicationData.getShortText() == null) {
currentLayoutHelper = new IconLayoutHelper();
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index a019e7b..e9b11db 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -103,7 +103,7 @@
@SuppressWarnings("deprecation") // b/251211092
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ MockitoAnnotations.openMocks(this);
mComplicationDrawable = new ComplicationDrawable();
mComplicationDrawable.setCallback(mMockDrawableCallback);
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
index 72795cd..f5f9868 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationRendererTest.java
@@ -110,7 +110,7 @@
@SuppressWarnings("deprecation") // b/251211092
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ MockitoAnnotations.openMocks(this);
mComplicationBounds = new Rect(0, 0, BOUNDS_WIDTH, BOUNDS_HEIGHT);
mComplicationRenderer = createRendererWithBounds(mComplicationBounds);
diff --git a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
index ac2c452..3ffc761 100644
--- a/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
+++ b/wear/watchface/watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/RoundedDrawableTest.java
@@ -55,7 +55,7 @@
@SuppressWarnings("deprecation") // b/251211092
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ MockitoAnnotations.openMocks(this);
mRoundedDrawable = new RoundedDrawable();
mBitmapDrawable =
new BitmapDrawable(
diff --git a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
index 941d1a0..af75b55 100644
--- a/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
+++ b/wear/watchface/watchface-data/src/main/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParams.java
@@ -75,7 +75,12 @@
/** Reserved field */
@ParcelField(101)
@Nullable
- ComponentName mAuxiliaryComponentName;
+ String mAuxiliaryComponentClassName;
+
+ /** Reserved field */
+ @ParcelField(102)
+ @Nullable
+ String mAuxiliaryComponentPackageName;
/** Used by VersionedParcelable. */
WallpaperInteractiveWatchFaceInstanceParams() {
@@ -93,7 +98,10 @@
mWatchUiState = watchUiState;
mUserStyle = userStyle;
mIdAndComplicationDataWireFormats = idAndComplicationDataWireFormats;
- mAuxiliaryComponentName = auxiliaryComponentName;
+ if (auxiliaryComponentName != null) {
+ mAuxiliaryComponentClassName = auxiliaryComponentName.getClassName();
+ mAuxiliaryComponentPackageName = auxiliaryComponentName.getPackageName();
+ }
}
@NonNull
diff --git a/wear/watchface/watchface-samples-minimal-complications/build.gradle b/wear/watchface/watchface-samples-minimal-complications/build.gradle
index ec59cdc..1bbe66d 100644
--- a/wear/watchface/watchface-samples-minimal-complications/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-complications/build.gradle
@@ -44,6 +44,7 @@
android {
defaultConfig {
+ applicationId "androidx.wear.watchface.samples.minimal.complications"
minSdkVersion 26
}
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
index f0b904f..4318358 100644
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-complications/src/main/AndroidManifest.xml
@@ -1,67 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <uses-feature android:name="android.hardware.type.watch" />
-
- <uses-permission android:name="android.permission.WAKE_LOCK" />
- <uses-permission
- android:name="com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA" />
-
- <application
- android:allowBackup="true"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
- android:supportsRtl="true"
- android:theme="@android:style/Theme.DeviceDefault"
- android:fullBackupContent="false">
-
- <activity
- android:name=".ConfigActivity"
- android:exported="true"
- android:label="@string/configuration_title">
- <intent-filter>
- <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
- <category
- android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <service
- android:name=".WatchFaceService"
- android:directBootAware="true"
- android:exported="true"
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="false"
+ android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:permission="android.permission.BIND_WALLPAPER">
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault">
- <intent-filter>
- <action android:name="android.service.wallpaper.WallpaperService" />
- <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
- </intent-filter>
+ <activity
+ android:name=".ConfigActivity"
+ android:exported="true"
+ android:label="@string/configuration_title">
+ <intent-filter>
+ <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/preview" />
+ <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
- <meta-data
- android:name="android.service.wallpaper"
- android:resource="@xml/watch_face" />
+ <service
+ android:name=".WatchFaceService"
+ android:directBootAware="true"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:permission="android.permission.BIND_WALLPAPER">
- <meta-data
- android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
- <meta-data
- android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
- android:value="true" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview" />
- <meta-data
- android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
- android:resource="@xml/xml_watchface" />
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
- </service>
+ <meta-data
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
- </application>
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+ android:value="true" />
+
+ <meta-data
+ android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+ android:resource="@xml/xml_watchface" />
+
+ </service>
+
+ </application>
+
+ <uses-feature android:name="android.hardware.type.watch" />
+ <uses-permission android:name="com.google.android.wearable.permission.RECEIVE_COMPLICATION_DATA" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java
deleted file mode 100644
index 8b254a9..0000000
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.complications;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
- private final Context mContext;
- private final String mTag;
- private final String mName;
-
- BaseFutureCallback(Context context, String tag, String name) {
- mContext = context;
- mTag = tag;
- mName = name;
- }
-
- @Override
- public void onPending() {
- Log.d(mTag, mName + ".onPending()");
- }
-
- @Override
- public void onSuccess(T value) {
- Log.d(mTag, mName + ".onSuccess(" + value + ")");
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
- Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onCancelled() {
- Log.d(mTag, mName + ".onCancelled()");
- Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onInterrupted() {
- Thread.currentThread().interrupt();
- Log.d(mTag, mName + ".onInterrupted()");
- Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
- }
-}
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
index 580fea5..f46584f 100644
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/ConfigActivity.java
@@ -18,8 +18,6 @@
import android.os.Build;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
@@ -27,6 +25,7 @@
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.lifecycle.FlowLiveDataConversions;
import androidx.wear.watchface.complications.ComplicationDataSourceInfo;
import androidx.wear.watchface.complications.data.ComplicationData;
@@ -35,22 +34,13 @@
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutionException;
/** Configuration activity for the watch face. */
public class ConfigActivity extends ComponentActivity {
private static final String TAG = "ConfigActivity";
- private Executor mMainExecutor = new Executor() {
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- @Override
- public void execute(Runnable runnable) {
- mHandler.post(runnable);
- }
- };
-
private TextView mComplicationProviderName;
private ImageView mComplicationPreview;
private ComplicationDrawable mComplicationPreviewDrawable;
@@ -58,23 +48,11 @@
@Nullable
private ListenableEditorSession mEditorSession;
- public ConfigActivity() {
- addCallback(
- ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
- new BaseFutureCallback<ListenableEditorSession>(
- this, TAG, "listenableCreateOnWatchEditingSession") {
- @Override
- public void onSuccess(ListenableEditorSession editorSession) {
- super.onSuccess(editorSession);
- setEditorSession(editorSession);
- }
- });
- }
-
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity_layout);
+ listenForEditorSession();
mComplicationProviderName = findViewById(R.id.complication_provider_name);
mComplicationPreview = findViewById(R.id.complication_preview);
@@ -88,8 +66,32 @@
}
}
- private void setEditorSession(ListenableEditorSession editorSession) {
- ConfigActivity.this.mEditorSession = editorSession;
+ @Override
+ protected void onDestroy() {
+ finish();
+ super.onDestroy();
+ }
+
+ private void listenForEditorSession() {
+ ListenableFuture<ListenableEditorSession> editorSessionFuture =
+ ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+ editorSessionFuture.addListener(() -> {
+ ListenableEditorSession editorSession;
+ try {
+ editorSession = editorSessionFuture.get();
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ return;
+ }
+ if (editorSession == null) {
+ return;
+ }
+ mEditorSession = editorSession;
+ observeComplications();
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private void observeComplications() {
FlowLiveDataConversions.asLiveData(mEditorSession.getComplicationsDataSourceInfo()).observe(
this,
complicationDataSourceInfoMap -> {
@@ -138,16 +140,7 @@
mEditorSession
.listenableOpenComplicationDataSourceChooser(
WatchFaceService.getComplicationId(getResources())
- ).addListener(() -> { /* Empty on purpose. */ }, mMainExecutor);
- }
-
- @Override
- protected void onDestroy() {
- finish();
- super.onDestroy();
- }
-
- private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
- FutureCallback.addCallback(future, callback, mMainExecutor);
+ ).addListener(() -> { /* Empty on purpose. */ },
+ ContextCompat.getMainExecutor(this));
}
}
diff --git a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java b/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java
deleted file mode 100644
index 1a629dd..0000000
--- a/wear/watchface/watchface-samples-minimal-complications/src/main/java/androidx/wear/watchface/samples/minimal/complications/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.complications;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
- static <T> void addCallback(
- ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
- if (!future.isDone()) {
- callback.onPending();
- }
- future.addListener(
- () -> {
- if (future.isCancelled()) {
- callback.onCancelled();
- } else {
- try {
- callback.onSuccess(future.get());
- } catch (InterruptedException e) {
- callback.onInterrupted();
- } catch (ExecutionException e) {
- callback.onFailure(e.getCause());
- }
- }
- },
- executor);
- }
-
- /** Called immediately if a callback is added to a future that is not yet done. */
- void onPending();
-
- /** Called if the future returns a value. */
- void onSuccess(T value);
-
- /** Called if the future throws an exception. */
- void onFailure(Throwable throwable);
-
- /** Called if the future is interrupted. */
- void onInterrupted();
-
- /** Called if the future is cancelled. */
- void onCancelled();
-}
diff --git a/wear/watchface/watchface-samples-minimal-instances/build.gradle b/wear/watchface/watchface-samples-minimal-instances/build.gradle
index 2931105..4249bd3 100644
--- a/wear/watchface/watchface-samples-minimal-instances/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-instances/build.gradle
@@ -43,6 +43,7 @@
android {
defaultConfig {
+ applicationId "androidx.wear.watchface.samples.minimal.instances"
minSdkVersion 26
}
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
index 2b1f8b1..52e37d3 100644
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-instances/src/main/AndroidManifest.xml
@@ -1,68 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <uses-feature android:name="android.hardware.type.watch" />
-
- <uses-permission android:name="android.permission.WAKE_LOCK" />
-
- <application
- android:allowBackup="true"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
- android:supportsRtl="true"
- android:theme="@android:style/Theme.DeviceDefault"
- android:fullBackupContent="false">
-
- <activity
- android:name="androidx.wear.watchface.samples.minimal.instances.ConfigActivity"
- android:exported="true"
- android:label="@string/configuration_title">
- <intent-filter>
- <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
- <category
- android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <service
- android:name="androidx.wear.watchface.samples.minimal.instances.WatchFaceService"
- android:directBootAware="true"
- android:exported="true"
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="false"
+ android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:permission="android.permission.BIND_WALLPAPER">
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault">
- <intent-filter>
- <action android:name="android.service.wallpaper.WallpaperService" />
- <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
- </intent-filter>
+ <activity
+ android:name=".ConfigActivity"
+ android:exported="true"
+ android:label="@string/configuration_title">
+ <intent-filter>
+ <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/preview" />
+ <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
- <meta-data
- android:name="android.service.wallpaper"
- android:resource="@xml/watch_face" />
+ <service
+ android:name=".WatchFaceService"
+ android:directBootAware="true"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:permission="android.permission.BIND_WALLPAPER">
- <meta-data
- android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
- <meta-data
- android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
- android:value="true" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview" />
- <meta-data
- android:name="androidx.wear.watchface.MULTIPLE_INSTANCES_ALLOWED"
- android:value="true" />
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
- <meta-data
- android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
- android:resource="@xml/xml_watchface" />
- </service>
+ <meta-data
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
- </application>
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+ android:value="true" />
+
+ <meta-data
+ android:name="androidx.wear.watchface.MULTIPLE_INSTANCES_ALLOWED"
+ android:value="true" />
+
+ <meta-data
+ android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+ android:resource="@xml/xml_watchface" />
+ </service>
+
+ </application>
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java
deleted file mode 100644
index 3f7d131..0000000
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.instances;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
- private final Context mContext;
- private final String mTag;
- private final String mName;
-
- BaseFutureCallback(Context context, String tag, String name) {
- mContext = context;
- mTag = tag;
- mName = name;
- }
-
- @Override
- public void onPending() {
- Log.d(mTag, mName + ".onPending()");
- }
-
- @Override
- public void onSuccess(T value) {
- Log.d(mTag, mName + ".onSuccess(" + value + ")");
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
- Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onCancelled() {
- Log.d(mTag, mName + ".onCancelled()");
- Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onInterrupted() {
- Thread.currentThread().interrupt();
- Log.d(mTag, mName + ".onInterrupted()");
- Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
- }
-}
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
index e53dbbd..fc4c23a 100644
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/ConfigActivity.java
@@ -27,6 +27,7 @@
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.wear.watchface.editor.ListenableEditorSession;
import androidx.wear.watchface.style.MutableUserStyle;
import androidx.wear.watchface.style.UserStyleSetting;
@@ -35,6 +36,7 @@
import com.google.common.util.concurrent.ListenableFuture;
import java.util.Objects;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
/** Configuration activity for the watch face. */
@@ -42,15 +44,6 @@
private static final String TAG = "ConfigActivity";
- private final Executor mMainExecutor = new Executor() {
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- @Override
- public void execute(Runnable runnable) {
- mHandler.post(runnable);
- }
- };
-
private CurvedTextView mInstanceId;
private TextView mStyleValue;
private final UserStyleSetting.Id mTimeStyleId = new UserStyleSetting.Id("TimeStyle");
@@ -58,25 +51,11 @@
@Nullable
private ListenableEditorSession mEditorSession;
- public ConfigActivity() {
- addCallback(
- ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
- new BaseFutureCallback<ListenableEditorSession>(
- this, TAG, "listenableCreateOnWatchEditingSession") {
- @Override
- public void onSuccess(ListenableEditorSession editorSession) {
- super.onSuccess(editorSession);
- mEditorSession = editorSession;
- updateInstanceId();
- updateStyleValue();
- }
- });
- }
-
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity_layout);
+ listenForEditorSession();
mInstanceId = findViewById(R.id.instance_id);
mStyleValue = findViewById(R.id.style_value);
@@ -96,6 +75,26 @@
super.onDestroy();
}
+ private void listenForEditorSession() {
+ ListenableFuture<ListenableEditorSession> editorSessionFuture =
+ ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+ editorSessionFuture.addListener(() -> {
+ ListenableEditorSession editorSession;
+ try {
+ editorSession = editorSessionFuture.get();
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ return;
+ }
+ if (editorSession == null) {
+ return;
+ }
+ mEditorSession = editorSession;
+ updateInstanceId();
+ updateStyleValue();
+ }, ContextCompat.getMainExecutor(this));
+ }
+
private void changeStyle() {
Log.d(TAG, "changeStyle");
if (mEditorSession == null) {
@@ -136,8 +135,4 @@
(ListOption) mEditorSession.getUserStyle().getValue().get(mTimeStyleId);
mStyleValue.setText(option.getDisplayName());
}
-
- private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
- FutureCallback.addCallback(future, callback, mMainExecutor);
- }
}
diff --git a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java b/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java
deleted file mode 100644
index 4c12b45..0000000
--- a/wear/watchface/watchface-samples-minimal-instances/src/main/java/androidx/wear/watchface/samples/minimal/instances/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.instances;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
- static <T> void addCallback(
- ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
- if (!future.isDone()) {
- callback.onPending();
- }
- future.addListener(
- () -> {
- if (future.isCancelled()) {
- callback.onCancelled();
- } else {
- try {
- callback.onSuccess(future.get());
- } catch (InterruptedException e) {
- callback.onInterrupted();
- } catch (ExecutionException e) {
- callback.onFailure(e.getCause());
- }
- }
- },
- executor);
- }
-
- /** Called immediately if a callback is added to a future that is not yet done. */
- void onPending();
-
- /** Called if the future returns a value. */
- void onSuccess(T value);
-
- /** Called if the future throws an exception. */
- void onFailure(Throwable throwable);
-
- /** Called if the future is interrupted. */
- void onInterrupted();
-
- /** Called if the future is cancelled. */
- void onCancelled();
-}
diff --git a/wear/watchface/watchface-samples-minimal-style/build.gradle b/wear/watchface/watchface-samples-minimal-style/build.gradle
index f2d59f2..df0cb91 100644
--- a/wear/watchface/watchface-samples-minimal-style/build.gradle
+++ b/wear/watchface/watchface-samples-minimal-style/build.gradle
@@ -41,6 +41,7 @@
android {
defaultConfig {
+ applicationId "androidx.wear.watchface.samples.minimal.style"
minSdkVersion 26
}
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml b/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
index 404d473..79fc1fc 100644
--- a/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface-samples-minimal-style/src/main/AndroidManifest.xml
@@ -1,64 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
- <uses-feature android:name="android.hardware.type.watch" />
-
- <uses-permission android:name="android.permission.WAKE_LOCK" />
-
- <application
- android:allowBackup="true"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name"
- android:supportsRtl="true"
- android:theme="@android:style/Theme.DeviceDefault"
- android:fullBackupContent="false">
-
- <activity
- android:name=".ConfigActivity"
- android:exported="true"
- android:label="@string/configuration_title">
- <intent-filter>
- <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
-
- <category
- android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
- <category android:name="android.intent.category.DEFAULT" />
- </intent-filter>
- </activity>
-
- <service
- android:name=".WatchFaceService"
- android:directBootAware="true"
- android:exported="true"
+ <application
+ android:allowBackup="true"
+ android:fullBackupContent="false"
+ android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:permission="android.permission.BIND_WALLPAPER">
+ android:supportsRtl="true"
+ android:theme="@android:style/Theme.DeviceDefault">
- <intent-filter>
- <action android:name="android.service.wallpaper.WallpaperService" />
- <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
- </intent-filter>
+ <activity
+ android:name=".ConfigActivity"
+ android:exported="true"
+ android:label="@string/configuration_title">
+ <intent-filter>
+ <action android:name="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
- <meta-data
- android:name="com.google.android.wearable.watchface.preview"
- android:resource="@drawable/preview" />
+ <category android:name="com.google.android.wearable.watchface.category.WEARABLE_CONFIGURATION" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
- <meta-data
- android:name="android.service.wallpaper"
- android:resource="@xml/watch_face" />
+ <service
+ android:name=".WatchFaceService"
+ android:directBootAware="true"
+ android:exported="true"
+ android:label="@string/app_name"
+ android:permission="android.permission.BIND_WALLPAPER">
- <meta-data
- android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
- android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+ <intent-filter>
+ <action android:name="android.service.wallpaper.WallpaperService" />
+ <category android:name="com.google.android.wearable.watchface.category.WATCH_FACE" />
+ </intent-filter>
- <meta-data
- android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
- android:value="true" />
+ <meta-data
+ android:name="com.google.android.wearable.watchface.preview"
+ android:resource="@drawable/preview" />
- <meta-data
- android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
- android:resource="@xml/xml_watchface" />
- </service>
+ <meta-data
+ android:name="android.service.wallpaper"
+ android:resource="@xml/watch_face" />
- </application>
+ <meta-data
+ android:name="com.google.android.wearable.watchface.wearableConfigurationAction"
+ android:value="androidx.wear.watchface.editor.action.WATCH_FACE_EDITOR" />
+
+ <meta-data
+ android:name="com.google.android.wearable.watchface.companionBuiltinConfigurationEnabled"
+ android:value="true" />
+
+ <meta-data
+ android:name="androidx.wear.watchface.XmlSchemaAndComplicationSlotsDefinition"
+ android:resource="@xml/xml_watchface" />
+ </service>
+
+ </application>
+
+ <uses-feature android:name="android.hardware.type.watch" />
+
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java
deleted file mode 100644
index f1499b7..0000000
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/BaseFutureCallback.java
+++ /dev/null
@@ -1,64 +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.wear.watchface.samples.minimal.style;
-
-import android.content.Context;
-import android.util.Log;
-import android.widget.Toast;
-
-/** A base class for a {@link FutureCallback} that logs the outcome. */
-abstract class BaseFutureCallback<T> implements FutureCallback<T> {
-
- private final Context mContext;
- private final String mTag;
- private final String mName;
-
- BaseFutureCallback(Context context, String tag, String name) {
- mContext = context;
- mTag = tag;
- mName = name;
- }
-
- @Override
- public void onPending() {
- Log.d(mTag, mName + ".onPending()");
- }
-
- @Override
- public void onSuccess(T value) {
- Log.d(mTag, mName + ".onSuccess(" + value + ")");
- }
-
- @Override
- public void onFailure(Throwable throwable) {
- Log.d(mTag, mName + ".onFailure(" + throwable.getMessage() + ")", throwable);
- Toast.makeText(mContext, "Failure", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onCancelled() {
- Log.d(mTag, mName + ".onCancelled()");
- Toast.makeText(mContext, "Cancelled", Toast.LENGTH_LONG).show();
- }
-
- @Override
- public void onInterrupted() {
- Thread.currentThread().interrupt();
- Log.d(mTag, mName + ".onInterrupted()");
- Toast.makeText(mContext, "Interrupted", Toast.LENGTH_LONG).show();
- }
-}
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
index 288ba72..ec29ff6 100644
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
+++ b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/ConfigActivity.java
@@ -20,59 +20,36 @@
import static androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption;
import android.os.Bundle;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
import android.widget.TextView;
import androidx.activity.ComponentActivity;
import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
import androidx.wear.watchface.editor.ListenableEditorSession;
import androidx.wear.watchface.style.MutableUserStyle;
import androidx.wear.watchface.style.UserStyleSetting;
import com.google.common.util.concurrent.ListenableFuture;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutionException;
/** Configuration activity for the watch face. */
public class ConfigActivity extends ComponentActivity {
private static final String TAG = "ConfigActivity";
- private final Executor mMainExecutor = new Executor() {
- private final Handler mHandler = new Handler(Looper.getMainLooper());
-
- @Override
- public void execute(Runnable runnable) {
- mHandler.post(runnable);
- }
- };
-
private TextView mStyleValue;
private final UserStyleSetting.Id mTimeStyleId = new UserStyleSetting.Id("TimeStyle");
@Nullable
private ListenableEditorSession mEditorSession;
- public ConfigActivity() {
- addCallback(
- ListenableEditorSession.listenableCreateOnWatchEditorSession(this),
- new BaseFutureCallback<ListenableEditorSession>(
- this, TAG, "listenableCreateOnWatchEditingSession") {
- @Override
- public void onSuccess(ListenableEditorSession editorSession) {
- super.onSuccess(editorSession);
- mEditorSession = editorSession;
- updateStyleValue();
- }
- });
- }
-
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.config_activity_layout);
+ listenForEditorSession();
mStyleValue = findViewById(R.id.style_value);
@@ -91,6 +68,25 @@
super.onDestroy();
}
+ private void listenForEditorSession() {
+ ListenableFuture<ListenableEditorSession> editorSessionFuture =
+ ListenableEditorSession.listenableCreateOnWatchEditorSession(this);
+ editorSessionFuture.addListener(() -> {
+ ListenableEditorSession editorSession;
+ try {
+ editorSession = editorSessionFuture.get();
+ } catch (ExecutionException | InterruptedException e) {
+ e.printStackTrace();
+ return;
+ }
+ if (editorSession == null) {
+ return;
+ }
+ mEditorSession = editorSession;
+ updateStyleValue();
+ }, ContextCompat.getMainExecutor(this));
+ }
+
private void changeStyle() {
Log.d(TAG, "changeStyle");
if (mEditorSession == null) {
@@ -124,8 +120,4 @@
(ListOption) mEditorSession.getUserStyle().getValue().get(mTimeStyleId);
mStyleValue.setText(option.getDisplayName());
}
-
- private <T> void addCallback(ListenableFuture<T> future, FutureCallback<T> callback) {
- FutureCallback.addCallback(future, callback, mMainExecutor);
- }
}
diff --git a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java b/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java
deleted file mode 100644
index 12f62de..0000000
--- a/wear/watchface/watchface-samples-minimal-style/src/main/java/androidx/wear/watchface/samples/minimal/style/FutureCallback.java
+++ /dev/null
@@ -1,63 +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.wear.watchface.samples.minimal.style;
-
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-/** A callback for a future that explicitly handles different types of outcomes. */
-interface FutureCallback<T> {
-
- static <T> void addCallback(
- ListenableFuture<T> future, FutureCallback<T> callback, Executor executor) {
- if (!future.isDone()) {
- callback.onPending();
- }
- future.addListener(
- () -> {
- if (future.isCancelled()) {
- callback.onCancelled();
- } else {
- try {
- callback.onSuccess(future.get());
- } catch (InterruptedException e) {
- callback.onInterrupted();
- } catch (ExecutionException e) {
- callback.onFailure(e.getCause());
- }
- }
- },
- executor);
- }
-
- /** Called immediately if a callback is added to a future that is not yet done. */
- void onPending();
-
- /** Called if the future returns a value. */
- void onSuccess(T value);
-
- /** Called if the future throws an exception. */
- void onFailure(Throwable throwable);
-
- /** Called if the future is interrupted. */
- void onInterrupted();
-
- /** Called if the future is cancelled. */
- void onCancelled();
-}
diff --git a/wear/watchface/watchface/api/current.txt b/wear/watchface/watchface/api/current.txt
index d19bba9..1319949 100644
--- a/wear/watchface/watchface/api/current.txt
+++ b/wear/watchface/watchface/api/current.txt
@@ -41,7 +41,7 @@
method @UiThread public Integer? getNameResourceId();
method public androidx.wear.watchface.CanvasComplication getRenderer();
method @UiThread public Integer? getScreenReaderNameResourceId();
- method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+ method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
method public boolean isActiveAt(java.time.Instant instant);
method @UiThread public boolean isEnabled();
@@ -63,7 +63,7 @@
property @UiThread public final Integer? nameResourceId;
property public final androidx.wear.watchface.CanvasComplication renderer;
property @UiThread public final Integer? screenReaderNameResourceId;
- property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+ property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
}
diff --git a/wear/watchface/watchface/api/public_plus_experimental_current.txt b/wear/watchface/watchface/api/public_plus_experimental_current.txt
index 7763240..8965e97 100644
--- a/wear/watchface/watchface/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface/api/public_plus_experimental_current.txt
@@ -55,7 +55,7 @@
method @UiThread public Integer? getNameResourceId();
method public androidx.wear.watchface.CanvasComplication getRenderer();
method @UiThread public Integer? getScreenReaderNameResourceId();
- method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+ method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
method public boolean isActiveAt(java.time.Instant instant);
method @UiThread public boolean isEnabled();
@@ -77,7 +77,7 @@
property @UiThread public final Integer? nameResourceId;
property public final androidx.wear.watchface.CanvasComplication renderer;
property @UiThread public final Integer? screenReaderNameResourceId;
- property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+ property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
}
diff --git a/wear/watchface/watchface/api/restricted_current.txt b/wear/watchface/watchface/api/restricted_current.txt
index d19bba9..1319949 100644
--- a/wear/watchface/watchface/api/restricted_current.txt
+++ b/wear/watchface/watchface/api/restricted_current.txt
@@ -41,7 +41,7 @@
method @UiThread public Integer? getNameResourceId();
method public androidx.wear.watchface.CanvasComplication getRenderer();
method @UiThread public Integer? getScreenReaderNameResourceId();
- method @UiThread public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
+ method public java.util.List<androidx.wear.watchface.complications.data.ComplicationType> getSupportedTypes();
method public androidx.wear.watchface.ComplicationTapFilter getTapFilter();
method public boolean isActiveAt(java.time.Instant instant);
method @UiThread public boolean isEnabled();
@@ -63,7 +63,7 @@
property @UiThread public final Integer? nameResourceId;
property public final androidx.wear.watchface.CanvasComplication renderer;
property @UiThread public final Integer? screenReaderNameResourceId;
- property @UiThread public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
+ property public final java.util.List<androidx.wear.watchface.complications.data.ComplicationType> supportedTypes;
property public final androidx.wear.watchface.ComplicationTapFilter tapFilter;
field public static final androidx.wear.watchface.ComplicationSlot.Companion Companion;
}
diff --git a/wear/watchface/watchface/samples/minimal/build.gradle b/wear/watchface/watchface/samples/minimal/build.gradle
index 8896dd1..f8f155d 100644
--- a/wear/watchface/watchface/samples/minimal/build.gradle
+++ b/wear/watchface/watchface/samples/minimal/build.gradle
@@ -37,6 +37,7 @@
android {
defaultConfig {
+ applicationId "androidx.wear.watchface.samples.minimal"
minSdkVersion 26
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index e22627f..d2186c6 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -28,10 +28,7 @@
import android.graphics.drawable.Icon
import android.view.SurfaceHolder
import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.annotation.VisibleForTesting
import androidx.wear.watchface.CanvasComplicationFactory
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.ComplicationSlot
@@ -44,6 +41,10 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -64,55 +65,12 @@
import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
import java.time.ZonedDateTime
import kotlin.math.cos
import kotlin.math.sin
-
-private const val CENTER_CIRCLE_DIAMETER_FRACTION = 0.03738f
-private const val OUTER_CIRCLE_STROKE_THICKNESS_FRACTION = 0.00467f
-private const val NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION = 0.00584f
-
-private const val GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION = 0.03738f
-private const val GAP_BETWEEN_HAND_AND_CENTER_FRACTION =
- 0.01869f + CENTER_CIRCLE_DIAMETER_FRACTION / 2.0f
-
-private const val HOUR_HAND_LENGTH_FRACTION = 0.21028f
-private const val HOUR_HAND_THICKNESS_FRACTION = 0.02336f
-private const val MINUTE_HAND_LENGTH_FRACTION = 0.3783f
-private const val MINUTE_HAND_THICKNESS_FRACTION = 0.0163f
-private const val SECOND_HAND_LENGTH_FRACTION = 0.37383f
-private const val SECOND_HAND_THICKNESS_FRACTION = 0.00934f
-
-const val NUMBER_RADIUS_FRACTION = 0.45f
-
-const val COLOR_STYLE_SETTING = "color_style_setting"
-const val RED_STYLE = "red_style"
-const val GREEN_STYLE = "green_style"
-const val BLUE_STYLE = "blue_style"
-const val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
-const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
-const val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
-const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
-const val NO_COMPLICATIONS = "NO_COMPLICATIONS"
-const val LEFT_COMPLICATION = "LEFT_COMPLICATION"
-const val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
-const val LEFT_AND_RIGHT_COMPLICATIONS = "LEFT_AND_RIGHT_COMPLICATIONS"
-
-/** How long each frame is displayed at expected frame rate. */
-private const val FRAME_PERIOD_MS: Long = 16L
-
-const val EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID = 101
-const val EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID = 102
-
-const val HOURS_DRAW_FREQ_MIN = 1L
-const val HOURS_DRAW_FREQ_MAX = 4L
-const val HOURS_DRAW_FREQ_DEFAULT = 3L
-
-const val CONFIGURABLE_DATA_SOURCE_PKG = "androidx.wear.watchface.complications.datasource.samples"
-const val CONFIGURABLE_DATA_SOURCE = "$CONFIGURABLE_DATA_SOURCE_PKG.ConfigurableDataSourceService"
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
/** A simple example canvas based analog watch face. NB this is open for testing. */
open class ExampleCanvasAnalogWatchFaceService : WatchFaceService() {
@@ -270,13 +228,15 @@
)
)
- internal val exampleFlavor by lazy {
+ private val exampleFlavor by lazy {
UserStyleFlavor(
"exampleFlavor",
- UserStyle(mapOf(
- colorStyleSetting to colorStyleSetting.getOptionForId(Option.Id(BLUE_STYLE)),
- watchHandLengthStyleSetting to DoubleRangeOption(1.0)
- )),
+ UserStyle(
+ mapOf(
+ colorStyleSetting to colorStyleSetting.getOptionForId(Option.Id(BLUE_STYLE)),
+ watchHandLengthStyleSetting to DoubleRangeOption(1.0)
+ )
+ ),
mapOf(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
DefaultComplicationDataSourcePolicy(
@@ -290,7 +250,8 @@
SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
ComplicationType.SHORT_TEXT
)
- ))
+ )
+ )
}
public override fun createUserStyleFlavors(
@@ -314,6 +275,8 @@
canvasComplicationFactory,
listOf(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -335,6 +298,8 @@
canvasComplicationFactory,
listOf(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -380,361 +345,420 @@
.setComplicationRationaleDialogIntent(
Intent(this, ComplicationRationalActivity::class.java)
)
-}
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleAnalogWatchCanvasRenderer(
- surfaceHolder: SurfaceHolder,
- private val context: Context,
- private var watchFaceColorStyle: WatchFaceColorStyle,
- currentUserStyleRepository: CurrentUserStyleRepository,
- watchState: WatchState,
- private val colorStyleSetting: ListUserStyleSetting,
- private val drawPipsStyleSetting: BooleanUserStyleSetting,
- private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
- private val hoursDrawFreqStyleSetting: LongRangeUserStyleSetting,
- private val complicationSlotsManager: ComplicationSlotsManager
-) : Renderer.CanvasRenderer(
- surfaceHolder,
- currentUserStyleRepository,
- watchState,
- CanvasType.HARDWARE,
- FRAME_PERIOD_MS,
- clearWithBackgroundTintBeforeRenderingHighlightLayer = true
-) {
- private val clockHandPaint = Paint().apply {
- isAntiAlias = true
- strokeWidth = context.resources.getDimensionPixelSize(
- R.dimen.clock_hand_stroke_width
- ).toFloat()
- }
+ @OptIn(WatchFaceExperimental::class)
+ @Suppress("Deprecation")
+ @RequiresApi(27)
+ private class ExampleAnalogWatchCanvasRenderer(
+ surfaceHolder: SurfaceHolder,
+ private val context: Context,
+ private var watchFaceColorStyle: WatchFaceColorStyle,
+ currentUserStyleRepository: CurrentUserStyleRepository,
+ watchState: WatchState,
+ private val colorStyleSetting: ListUserStyleSetting,
+ private val drawPipsStyleSetting: BooleanUserStyleSetting,
+ private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
+ private val hoursDrawFreqStyleSetting: LongRangeUserStyleSetting,
+ private val complicationSlotsManager: ComplicationSlotsManager
+ ) : Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.HARDWARE,
+ FRAME_PERIOD_MS,
+ clearWithBackgroundTintBeforeRenderingHighlightLayer = true
+ ) {
+ private val clockHandPaint = Paint().apply {
+ isAntiAlias = true
+ strokeWidth = context.resources.getDimensionPixelSize(
+ R.dimen.clock_hand_stroke_width
+ ).toFloat()
+ }
- private val outerElementPaint = Paint().apply {
- isAntiAlias = true
- }
+ private val outerElementPaint = Paint().apply {
+ isAntiAlias = true
+ }
- private val textPaint = Paint().apply {
- isAntiAlias = true
- textSize = context.resources.getDimensionPixelSize(R.dimen.hour_mark_size).toFloat()
- }
+ private val textPaint = Paint().apply {
+ isAntiAlias = true
+ textSize = context.resources.getDimensionPixelSize(R.dimen.hour_mark_size).toFloat()
+ }
- private lateinit var hourHandFill: Path
- private lateinit var hourHandBorder: Path
- private lateinit var minuteHandFill: Path
- private lateinit var minuteHandBorder: Path
- private lateinit var secondHand: Path
+ private lateinit var hourHandFill: Path
+ private lateinit var hourHandBorder: Path
+ private lateinit var minuteHandFill: Path
+ private lateinit var minuteHandBorder: Path
+ private lateinit var secondHand: Path
- private var drawHourPips = true
- private var watchHandScale = 1.0f
- private var hoursDrawFreq = HOURS_DRAW_FREQ_DEFAULT.toInt()
+ private var drawHourPips = true
+ private var watchHandScale = 1.0f
+ private var hoursDrawFreq = HOURS_DRAW_FREQ_DEFAULT.toInt()
- init {
- CoroutineScope(Dispatchers.Main.immediate).launch {
- currentUserStyleRepository.userStyle.collect { userStyle ->
- watchFaceColorStyle = WatchFaceColorStyle.create(
- context,
- userStyle[colorStyleSetting]!!.toString()
- )
+ init {
+ CoroutineScope(Dispatchers.Main.immediate).launch {
+ currentUserStyleRepository.userStyle.collect { userStyle ->
+ watchFaceColorStyle = WatchFaceColorStyle.create(
+ context,
+ userStyle[colorStyleSetting]!!.toString()
+ )
- // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
- // of the styles are defined in XML so we need to replace the complication's
- // drawables.
- for ((_, complication) in complicationSlotsManager.complicationSlots) {
- (complication.renderer as CanvasComplicationDrawable).drawable =
- watchFaceColorStyle.getDrawable(context)!!
- }
+ // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
+ // of the styles are defined in XML so we need to replace the complication's
+ // drawables.
+ for ((_, complication) in complicationSlotsManager.complicationSlots) {
+ (complication.renderer as CanvasComplicationDrawable).drawable =
+ watchFaceColorStyle.getDrawable(context)!!
+ }
- drawHourPips = (userStyle[drawPipsStyleSetting]!! as BooleanOption).value
- watchHandScale =
- (userStyle[watchHandLengthStyleSettingDouble]!! as DoubleRangeOption)
- .value.toFloat()
- hoursDrawFreq = (userStyle[hoursDrawFreqStyleSetting]!! as LongRangeOption)
+ drawHourPips = (userStyle[drawPipsStyleSetting]!! as BooleanOption).value
+ watchHandScale =
+ (userStyle[watchHandLengthStyleSettingDouble]!! as DoubleRangeOption)
+ .value.toFloat()
+ hoursDrawFreq = (userStyle[hoursDrawFreqStyleSetting]!! as LongRangeOption)
.value.toInt()
- watchfaceColors = WatchFaceColors(
- Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
- Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
- Color.valueOf(Color.DKGRAY)
+ watchfaceColors = WatchFaceColors(
+ Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
+ Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
+ Color.valueOf(Color.DKGRAY)
+ )
+ }
+ }
+ }
+
+ override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+ val style = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+ watchFaceColorStyle.ambientStyle
+ } else {
+ watchFaceColorStyle.activeStyle
+ }
+
+ canvas.drawColor(style.backgroundColor)
+
+ // We don't need to check renderParameters.watchFaceWatchFaceLayers because
+ // CanvasComplicationDrawable does that for us.
+ for ((_, complication) in complicationSlotsManager.complicationSlots) {
+ if (complication.enabled) {
+ complication.render(canvas, zonedDateTime, renderParameters)
+ }
+ }
+
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)
+ ) {
+ drawClockHands(canvas, bounds, zonedDateTime, style)
+ }
+
+ if (renderParameters.drawMode != DrawMode.AMBIENT &&
+ renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE) && drawHourPips
+ ) {
+ drawNumberStyleOuterElement(canvas, bounds, style)
+ }
+ }
+
+ override fun renderHighlightLayer(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime
+ ) {
+ for ((_, complication) in complicationSlotsManager.complicationSlots) {
+ if (complication.enabled) {
+ complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+ }
+ }
+ }
+
+ private fun drawClockHands(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime,
+ style: ColorStyle
+ ) {
+ recalculateClockHands(bounds)
+ val hours = (zonedDateTime.hour % 12).toFloat()
+ val minutes = zonedDateTime.minute.toFloat()
+ val seconds = zonedDateTime.second.toFloat() +
+ (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+
+ val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+ val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+
+ canvas.save()
+
+ recalculateClockHands(bounds)
+
+ if (renderParameters.drawMode == DrawMode.AMBIENT) {
+ clockHandPaint.style = Paint.Style.STROKE
+ clockHandPaint.color = style.primaryColor
+ canvas.scale(
+ watchHandScale,
+ watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.drawPath(hourHandBorder, clockHandPaint)
+ canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.scale(
+ 1.0f / watchHandScale,
+ 1.0f / watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+
+ clockHandPaint.color = style.secondaryColor
+ canvas.scale(
+ watchHandScale,
+ watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.drawPath(minuteHandBorder, clockHandPaint)
+ canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.scale(
+ 1.0f / watchHandScale,
+ 1.0f / watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ } else {
+ clockHandPaint.style = Paint.Style.FILL
+ clockHandPaint.color = style.primaryColor
+ canvas.scale(
+ watchHandScale,
+ watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.drawPath(hourHandFill, clockHandPaint)
+ canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.scale(
+ 1.0f / watchHandScale,
+ 1.0f / watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+
+ clockHandPaint.color = style.secondaryColor
+ canvas.scale(
+ watchHandScale,
+ watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.drawPath(minuteHandFill, clockHandPaint)
+ canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.scale(
+ 1.0f / watchHandScale,
+ 1.0f / watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+
+ val secondsRot = seconds / 60.0f * 360.0f
+
+ clockHandPaint.color = style.secondaryColor
+ canvas.scale(
+ watchHandScale,
+ watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
+ )
+ canvas.rotate(secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.drawPath(secondHand, clockHandPaint)
+ canvas.rotate(-secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
+ canvas.scale(
+ 1.0f / watchHandScale,
+ 1.0f / watchHandScale,
+ bounds.exactCenterX(),
+ bounds.exactCenterY()
)
}
- }
- }
- override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
- val style = if (renderParameters.drawMode == DrawMode.AMBIENT) {
- watchFaceColorStyle.ambientStyle
- } else {
- watchFaceColorStyle.activeStyle
+ canvas.restore()
}
- canvas.drawColor(style.backgroundColor)
+ private fun recalculateClockHands(bounds: Rect) {
+ val rx = 1.5f
+ val ry = 1.5f
- // We don't need to check renderParameters.watchFaceWatchFaceLayers because
- // CanvasComplicationDrawable does that for us.
- for ((_, complication) in complicationSlotsManager.complicationSlots) {
- if (complication.enabled) {
- complication.render(canvas, zonedDateTime, renderParameters)
- }
- }
-
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)
- ) {
- drawClockHands(canvas, bounds, zonedDateTime, style)
- }
-
- if (renderParameters.drawMode != DrawMode.AMBIENT &&
- renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE) && drawHourPips
- ) {
- drawNumberStyleOuterElement(canvas, bounds, style)
- }
- }
-
- override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
- for ((_, complication) in complicationSlotsManager.complicationSlots) {
- if (complication.enabled) {
- complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
- }
- }
- }
-
- private fun drawClockHands(
- canvas: Canvas,
- bounds: Rect,
- zonedDateTime: ZonedDateTime,
- style: ColorStyle
- ) {
- recalculateClockHands(bounds)
- val hours = (zonedDateTime.hour % 12).toFloat()
- val minutes = zonedDateTime.minute.toFloat()
- val seconds = zonedDateTime.second.toFloat() +
- (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
- val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
- val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
-
- canvas.save()
-
- recalculateClockHands(bounds)
-
- if (renderParameters.drawMode == DrawMode.AMBIENT) {
- clockHandPaint.style = Paint.Style.STROKE
- clockHandPaint.color = style.primaryColor
- canvas.scale(
- watchHandScale,
- watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.drawPath(hourHandBorder, clockHandPaint)
- canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.scale(
- 1.0f / watchHandScale,
- 1.0f / watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
-
- clockHandPaint.color = style.secondaryColor
- canvas.scale(
- watchHandScale,
- watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.drawPath(minuteHandBorder, clockHandPaint)
- canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.scale(
- 1.0f / watchHandScale,
- 1.0f / watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- } else {
- clockHandPaint.style = Paint.Style.FILL
- clockHandPaint.color = style.primaryColor
- canvas.scale(
- watchHandScale,
- watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- canvas.rotate(hourRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.drawPath(hourHandFill, clockHandPaint)
- canvas.rotate(-hourRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.scale(
- 1.0f / watchHandScale,
- 1.0f / watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
-
- clockHandPaint.color = style.secondaryColor
- canvas.scale(
- watchHandScale,
- watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- canvas.rotate(minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.drawPath(minuteHandFill, clockHandPaint)
- canvas.rotate(-minuteRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.scale(
- 1.0f / watchHandScale,
- 1.0f / watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
-
- val secondsRot = seconds / 60.0f * 360.0f
-
- clockHandPaint.color = style.secondaryColor
- canvas.scale(
- watchHandScale,
- watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- canvas.rotate(secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.drawPath(secondHand, clockHandPaint)
- canvas.rotate(-secondsRot, bounds.exactCenterX(), bounds.exactCenterY())
- canvas.scale(
- 1.0f / watchHandScale,
- 1.0f / watchHandScale,
- bounds.exactCenterX(),
- bounds.exactCenterY()
- )
- }
-
- canvas.restore()
- }
-
- private fun recalculateClockHands(bounds: Rect) {
- val rx = 1.5f
- val ry = 1.5f
-
- hourHandBorder =
- createClockHand(bounds, HOUR_HAND_LENGTH_FRACTION, HOUR_HAND_THICKNESS_FRACTION, rx, ry)
-
- minuteHandBorder =
- createClockHand(
- bounds, MINUTE_HAND_LENGTH_FRACTION, MINUTE_HAND_THICKNESS_FRACTION, rx, ry
- )
-
- hourHandFill =
- createClockHand(
- bounds,
- HOUR_HAND_LENGTH_FRACTION,
- HOUR_HAND_THICKNESS_FRACTION,
- rx,
- ry
- )
-
- minuteHandFill =
- createClockHand(
- bounds,
- MINUTE_HAND_LENGTH_FRACTION,
- MINUTE_HAND_THICKNESS_FRACTION,
- rx,
- ry
- )
-
- secondHand =
- createClockHand(
- bounds,
- SECOND_HAND_LENGTH_FRACTION,
- SECOND_HAND_THICKNESS_FRACTION,
- 0.0f,
- 0.0f
- )
- }
-
- /**
- * Returns a round rect clock hand if {@code rx} and {@code ry} equals to 0, otherwise return a
- * rect clock hand.
- *
- * @param bounds The bounds use to determine the coordinate of the clock hand.
- * @param length Clock hand's length, in fraction of {@code bounds.width()}.
- * @param thickness Clock hand's thickness, in fraction of {@code bounds.width()}.
- * @param rx The x-radius of the rounded corners on the round-rectangle.
- * @param ry The y-radius of the rounded corners on the round-rectangle.
- */
- private fun createClockHand(
- bounds: Rect,
- length: Float,
- thickness: Float,
- rx: Float,
- ry: Float
- ): Path {
- val width = bounds.width()
- val cx = bounds.exactCenterX()
- val cy = bounds.exactCenterY()
- val left = cx - thickness / 2 * width
- val top = cy - (GAP_BETWEEN_HAND_AND_CENTER_FRACTION + length) * width
- val right = cx + thickness / 2 * width
- val bottom = cy - GAP_BETWEEN_HAND_AND_CENTER_FRACTION * width
- val path = Path()
- if (rx != 0.0f || ry != 0.0f) {
- path.addRoundRect(left, top, right, bottom, rx, ry, Path.Direction.CW)
- } else {
- path.addRect(left, top, right, bottom, Path.Direction.CW)
- }
- return path
- }
-
- private fun drawNumberStyleOuterElement(canvas: Canvas, bounds: Rect, style: ColorStyle) {
- val textBounds = Rect()
- textPaint.color = style.outerElementColor
- for (i in 12 downTo 1 step hoursDrawFreq) {
- val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
- val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
- val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
- val mark = i.toString()
- textPaint.getTextBounds(mark, 0, mark.length, textBounds)
- canvas.drawText(
- mark,
- bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
- bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
- textPaint
- )
- }
-
- // Draws the circle for the remain hour indicators.
- outerElementPaint.strokeWidth = OUTER_CIRCLE_STROKE_THICKNESS_FRACTION * bounds.width()
- outerElementPaint.color = style.outerElementColor
- canvas.save()
- for (i in 0 until 12) {
- if (i % hoursDrawFreq != 0) {
- drawTopMiddleCircle(
- canvas,
+ hourHandBorder =
+ createClockHand(
bounds,
- NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION
+ HOUR_HAND_LENGTH_FRACTION,
+ HOUR_HAND_THICKNESS_FRACTION,
+ rx,
+ ry
+ )
+
+ minuteHandBorder =
+ createClockHand(
+ bounds, MINUTE_HAND_LENGTH_FRACTION, MINUTE_HAND_THICKNESS_FRACTION, rx, ry
+ )
+
+ hourHandFill =
+ createClockHand(
+ bounds,
+ HOUR_HAND_LENGTH_FRACTION,
+ HOUR_HAND_THICKNESS_FRACTION,
+ rx,
+ ry
+ )
+
+ minuteHandFill =
+ createClockHand(
+ bounds,
+ MINUTE_HAND_LENGTH_FRACTION,
+ MINUTE_HAND_THICKNESS_FRACTION,
+ rx,
+ ry
+ )
+
+ secondHand =
+ createClockHand(
+ bounds,
+ SECOND_HAND_LENGTH_FRACTION,
+ SECOND_HAND_THICKNESS_FRACTION,
+ 0.0f,
+ 0.0f
+ )
+ }
+
+ /**
+ * Returns a round rect clock hand if {@code rx} and {@code ry} equals to 0, otherwise return a
+ * rect clock hand.
+ *
+ * @param bounds The bounds use to determine the coordinate of the clock hand.
+ * @param length Clock hand's length, in fraction of {@code bounds.width()}.
+ * @param thickness Clock hand's thickness, in fraction of {@code bounds.width()}.
+ * @param rx The x-radius of the rounded corners on the round-rectangle.
+ * @param ry The y-radius of the rounded corners on the round-rectangle.
+ */
+ private fun createClockHand(
+ bounds: Rect,
+ length: Float,
+ thickness: Float,
+ rx: Float,
+ ry: Float
+ ): Path {
+ val width = bounds.width()
+ val cx = bounds.exactCenterX()
+ val cy = bounds.exactCenterY()
+ val left = cx - thickness / 2 * width
+ val top = cy - (GAP_BETWEEN_HAND_AND_CENTER_FRACTION + length) * width
+ val right = cx + thickness / 2 * width
+ val bottom = cy - GAP_BETWEEN_HAND_AND_CENTER_FRACTION * width
+ val path = Path()
+ if (rx != 0.0f || ry != 0.0f) {
+ path.addRoundRect(left, top, right, bottom, rx, ry, Path.Direction.CW)
+ } else {
+ path.addRect(left, top, right, bottom, Path.Direction.CW)
+ }
+ return path
+ }
+
+ private fun drawNumberStyleOuterElement(canvas: Canvas, bounds: Rect, style: ColorStyle) {
+ val textBounds = Rect()
+ textPaint.color = style.outerElementColor
+ for (i in 12 downTo 1 step hoursDrawFreq) {
+ val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
+ val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+ val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+ val mark = i.toString()
+ textPaint.getTextBounds(mark, 0, mark.length, textBounds)
+ canvas.drawText(
+ mark,
+ bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
+ bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+ textPaint
)
}
- canvas.rotate(360.0f / 12.0f, bounds.exactCenterX(), bounds.exactCenterY())
+
+ // Draws the circle for the remain hour indicators.
+ outerElementPaint.strokeWidth = OUTER_CIRCLE_STROKE_THICKNESS_FRACTION * bounds.width()
+ outerElementPaint.color = style.outerElementColor
+ canvas.save()
+ for (i in 0 until 12) {
+ if (i % hoursDrawFreq != 0) {
+ drawTopMiddleCircle(
+ canvas,
+ bounds,
+ NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION
+ )
+ }
+ canvas.rotate(360.0f / 12.0f, bounds.exactCenterX(), bounds.exactCenterY())
+ }
+ canvas.restore()
}
- canvas.restore()
+
+ /** Draws the outer circle on the top middle of the given bounds. */
+ private fun drawTopMiddleCircle(
+ canvas: Canvas,
+ bounds: Rect,
+ radiusFraction: Float
+ ) {
+ outerElementPaint.style = Paint.Style.FILL_AND_STROKE
+
+ val cx = 0.5f * bounds.width().toFloat()
+ val cy =
+ bounds.width() * (GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION + radiusFraction)
+
+ canvas.drawCircle(
+ cx,
+ cy,
+ radiusFraction * bounds.width(),
+ outerElementPaint
+ )
+ }
}
- /** Draws the outer circle on the top middle of the given bounds. */
- private fun drawTopMiddleCircle(
- canvas: Canvas,
- bounds: Rect,
- radiusFraction: Float
- ) {
- outerElementPaint.style = Paint.Style.FILL_AND_STROKE
+ @VisibleForTesting
+ companion object {
+ private const val CENTER_CIRCLE_DIAMETER_FRACTION = 0.03738f
+ private const val OUTER_CIRCLE_STROKE_THICKNESS_FRACTION = 0.00467f
+ private const val NUMBER_STYLE_OUTER_CIRCLE_RADIUS_FRACTION = 0.00584f
- val cx = 0.5f * bounds.width().toFloat()
- val cy = bounds.width() * (GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION + radiusFraction)
+ private const val GAP_BETWEEN_OUTER_CIRCLE_AND_BORDER_FRACTION = 0.03738f
+ private const val GAP_BETWEEN_HAND_AND_CENTER_FRACTION =
+ 0.01869f + CENTER_CIRCLE_DIAMETER_FRACTION / 2.0f
- canvas.drawCircle(
- cx,
- cy,
- radiusFraction * bounds.width(),
- outerElementPaint
- )
+ private const val HOUR_HAND_LENGTH_FRACTION = 0.21028f
+ private const val HOUR_HAND_THICKNESS_FRACTION = 0.02336f
+ private const val MINUTE_HAND_LENGTH_FRACTION = 0.3783f
+ private const val MINUTE_HAND_THICKNESS_FRACTION = 0.0163f
+ private const val SECOND_HAND_LENGTH_FRACTION = 0.37383f
+ private const val SECOND_HAND_THICKNESS_FRACTION = 0.00934f
+
+ private const val NUMBER_RADIUS_FRACTION = 0.45f
+
+ const val COLOR_STYLE_SETTING = "color_style_setting"
+ private const val RED_STYLE = "red_style"
+ const val GREEN_STYLE = "green_style"
+ const val BLUE_STYLE = "blue_style"
+ const val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
+ const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+ const val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
+ private const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
+ const val NO_COMPLICATIONS = "NO_COMPLICATIONS"
+ const val LEFT_COMPLICATION = "LEFT_COMPLICATION"
+ private const val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
+ private const val LEFT_AND_RIGHT_COMPLICATIONS = "LEFT_AND_RIGHT_COMPLICATIONS"
+
+ /** How long each frame is displayed at expected frame rate. */
+ private const val FRAME_PERIOD_MS: Long = 16L
+
+ const val EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID = 101
+ const val EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID = 102
+
+ private const val HOURS_DRAW_FREQ_MIN = 1L
+ private const val HOURS_DRAW_FREQ_MAX = 4L
+ private const val HOURS_DRAW_FREQ_DEFAULT = 3L
+
+ const val CONFIGURABLE_DATA_SOURCE_PKG =
+ "androidx.wear.watchface.complications.datasource.samples"
+ const val CONFIGURABLE_DATA_SOURCE =
+ "$CONFIGURABLE_DATA_SOURCE_PKG.ConfigurableDataSourceService"
}
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index da22db5..3fd4f52 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -38,10 +38,6 @@
import android.view.animation.PathInterpolator
import androidx.annotation.ColorInt
import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.CanvasComplicationFactory
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.ComplicationSlot
@@ -54,6 +50,10 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -62,407 +62,13 @@
import androidx.wear.watchface.style.UserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
import java.time.ZonedDateTime
import kotlin.math.max
import kotlin.math.min
-
-// Render at approximately 60fps in interactive mode.
-private const val INTERACTIVE_UPDATE_RATE_MS = 16L
-
-internal class Vec2f(val x: Float, val y: Float)
-
-// Constants for the size of complication.
-internal val CIRCLE_COMPLICATION_DIAMETER_FRACTION = Vec2f(0.252f, 0.252f)
-internal val ROUND_RECT_COMPLICATION_SIZE_FRACTION = Vec2f(0.645f, 0.168f)
-
-// Constants for the upper complication location.
-internal val UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
-internal val UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
-
-// Constants for the lower complication location.
-internal val LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
-internal val LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
-
-// Constants for the left complication location.
-internal val LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.177f, 0.5f)
-
-// Constants for the right complication location.
-internal val RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.823f, 0.5f)
-
-// Constants for the clock digits' position, based on the height and width of given bounds.
-internal val MARGIN_FRACTION_WITHOUT_COMPLICATION = Vec2f(0.2f, 0.2f)
-internal val MARGIN_FRACTION_WITH_COMPLICATION = Vec2f(0.4f, 0.4f)
-
-enum class ComplicationID {
- UPPER,
- RIGHT,
- LOWER,
- LEFT,
- BACKGROUND
-}
-
-internal val VERTICAL_COMPLICATION_IDS = arrayOf(
- ComplicationID.UPPER.ordinal,
- ComplicationID.LOWER.ordinal
-)
-internal val HORIZONTAL_COMPLICATION_IDS = arrayOf(
- ComplicationID.LEFT.ordinal,
- ComplicationID.RIGHT.ordinal
-)
-internal val FOREGROUND_COMPLICATION_IDS = arrayOf(
- ComplicationID.UPPER.ordinal, ComplicationID.RIGHT.ordinal,
- ComplicationID.LOWER.ordinal, ComplicationID.LEFT.ordinal
-)
-
-// The name of the font used for drawing the text in the digit watch face.
-private const val DIGITAL_TYPE_FACE = "sans-serif-condensed-light"
-
-// The width of the large digit bitmaps, as a fraction of their height.
-private const val DIGIT_WIDTH_FRACTION = 0.65f
-
-// The height of the small digits (used for minutes and seconds), given as a fraction of the height
-// of the large digits.
-private const val SMALL_DIGIT_SIZE_FRACTION = 0.45f
-
-// The width of the small digit bitmaps, as a fraction of their height.
-private const val SMALL_DIGIT_WIDTH_FRACTION = 0.7f
-
-// The padding at the top and bottom of the digit bitmaps, given as a fraction of the height.
-// Needed as some characters may ascend or descend slightly (e.g. "8").
-private const val DIGIT_PADDING_FRACTION = 0.05f
-
-// The gap between the hours and the minutes/seconds, given as a fraction of the width of the large
-// digits.
-private const val GAP_WIDTH_FRACTION = 0.1f
-
-// A string containing all digits, used to measure their height.
-private const val ALL_DIGITS = "0123456789"
-
-internal val DIGITS = ALL_DIGITS.toCharArray().map { it.toString() }
-
-// Changing digits are animated. This enum is used to label the start and end animation parameters.
-internal enum class DigitMode {
- OUTGOING,
- INCOMING
-}
-
-// The start and end times of the animations, expressed as a fraction of a second.
-// (So 0.5 means that the animation of that digit will begin half-way through the second).
-// Note that because we only cache one digit of each type, the current and next times must
-// not overlap.
-internal val DIGIT_ANIMATION_START_TIME_FRACTION = mapOf(
- DigitMode.OUTGOING to 0.5f,
- DigitMode.INCOMING to 0.667f
-)
-internal val DIGIT_ANIMATION_END_TIME = mapOf(
- DigitMode.OUTGOING to 0.667f,
- DigitMode.INCOMING to 1f
-)
-internal const val POSITION_ANIMATION_START_TIME = 0.0833f
-private const val POSITION_ANIMATION_END_TIME = 0.5833f
-
-// Parameters governing the animation of the current and next digits. NB Scale is a size multiplier.
-// The first index is the values for the outgoing digit, and the second index for the incoming
-// digit. If seconds are changing from 1 -> 2 for example, the 1 will scale from 1f to 0.65f, and
-// rotate from 0f to 82f. The 2 will scale from 0.65f to 1f, and rotate from -97f to 0f.
-private val DIGIT_SCALE_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.65f)
-private val DIGIT_SCALE_END = mapOf(DigitMode.OUTGOING to 0.65f, DigitMode.INCOMING to 1f)
-private val DIGIT_ROTATE_START_DEGREES = mapOf(
- DigitMode.OUTGOING to 0f,
- DigitMode.INCOMING to -97f
-)
-private val DIGIT_ROTATE_END_DEGREES = mapOf(DigitMode.OUTGOING to 82f, DigitMode.INCOMING to 0f)
-private val DIGIT_OPACITY_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.07f)
-private val DIGIT_OPACITY_END = mapOf(DigitMode.OUTGOING to 0f, DigitMode.INCOMING to 1f)
-
-// The offset used to stagger the animation when multiple digits are animating at the same time.
-private const val TIME_OFFSET_SECONDS_PER_DIGIT_TYPE = -5 / 60f
-
-// The duration of the ambient mode change animation.
-private const val AMBIENT_TRANSITION_MS = 333L
-
-private val DIGIT_SCALE_INTERPOLATOR = mapOf(
- DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 0.67f, 1f),
- DigitMode.INCOMING to PathInterpolator(0.33f, 0f, 0.2f, 1f)
-)
-private val DIGIT_ROTATION_INTERPOLATOR = mapOf(
- DigitMode.OUTGOING to PathInterpolator(0.57f, 0f, 0.73f, 0.49f),
- DigitMode.INCOMING to PathInterpolator(0.15f, 0.49f, 0.37f, 1f)
-)
-private val DIGIT_OPACITY_INTERPOLATOR = mapOf(
- DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 1f, 1f),
- DigitMode.INCOMING to PathInterpolator(0f, 0f, 0.2f, 1f)
-)
-internal val CENTERING_ADJUSTMENT_INTERPOLATOR =
- PathInterpolator(0.4f, 0f, 0.2f, 1f)
-
-@ColorInt
-internal fun colorRgb(red: Float, green: Float, blue: Float) =
- 0xff000000.toInt() or
- ((red * 255.0f + 0.5f).toInt() shl 16) or
- ((green * 255.0f + 0.5f).toInt() shl 8) or
- (blue * 255.0f + 0.5f).toInt()
-
-internal fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
-
-internal fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
-
-internal fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
-
-/**
- * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
- * `backgroundColor` background.
- *
- * @param color the foreground color
- * @param alphaFraction the fraction of the alpha value, range from 0 to 1
- * @param backgroundColor the background color
- */
-internal fun getRGBColor(
- @ColorInt color: Int,
- alphaFraction: Float,
- @ColorInt backgroundColor: Int
-): Int {
- return colorRgb(
- lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
- lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
- lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
- )
-}
-
-internal enum class DigitType {
- HOUR_TENS,
- HOUR_UNITS,
- MINUTE_TENS,
- MINUTE_UNITS,
- SECOND_TENS,
- SECOND_UNITS
-}
-
-/** A class to provide string representations of each of the digits of a given time. */
-internal class DigitStrings {
- private var hourTens = ""
- private var hourUnits = ""
- private var minuteTens = ""
- private var minuteUnits = ""
- private var secondTens = ""
- private var secondUnits = ""
-
- /** Sets the time represented by this instance. */
- fun set(zonedDateTime: ZonedDateTime, is24Hour: Boolean) {
- if (is24Hour) {
- val hourValue = zonedDateTime.hour
- hourTens = getTensDigitString(hourValue, true)
- hourUnits = getUnitsDigitString(hourValue)
- } else {
- var hourValue = zonedDateTime.hour % 12
- // We should show 12 for noon and midnight.
- if (hourValue == 0) {
- hourValue = 12
- }
- hourTens = getTensDigitString(hourValue, false)
- hourUnits = getUnitsDigitString(hourValue)
- }
-
- val minuteValue = zonedDateTime.minute
- minuteTens = getTensDigitString(minuteValue, true)
- minuteUnits = getUnitsDigitString(minuteValue)
- val secondsValue = zonedDateTime.second
- secondTens = getTensDigitString(secondsValue, true)
- secondUnits = getUnitsDigitString(secondsValue)
- }
-
- /** Returns a string representing the specified digit of the time represented by this object. */
- fun get(digitType: DigitType): String {
- return when (digitType) {
- DigitType.HOUR_TENS -> hourTens
- DigitType.HOUR_UNITS -> hourUnits
- DigitType.MINUTE_TENS -> minuteTens
- DigitType.MINUTE_UNITS -> minuteUnits
- DigitType.SECOND_TENS -> secondTens
- DigitType.SECOND_UNITS -> secondUnits
- }
- }
-
- /**
- * Returns the number of hour digits in this object. If the representation is 24-hour, this will
- * always return 2. If 12-hour, this will return 1 or 2.
- */
- fun getNumberOfHoursDigits(): Int {
- return if (hourTens == "") 1 else 2
- }
-
- /**
- * Returns a {@link String} representing the tens digit of the provided non-negative {@code
- * value}. If {@code padWithZeroes} is true, returns zero if {@code value} < 10. If {@code
- * padWithZeroes} is false, returns an empty string if {@code value} < 10.
- */
- private fun getTensDigitString(value: Int, padWithZeroes: Boolean): String {
- if (value < 10 && !padWithZeroes) {
- return ""
- }
- // We don't use toString() because during draw calls we don't want to avoid allocating objects.
- return DIGITS[(value / 10) % 10]
- }
-
- /**
- * Returns a {@link String} representing the units digit of the provided non-negative {@code
- * value}.
- */
- private fun getUnitsDigitString(value: Int): String {
- // We don't use toString() because during draw calls we don't want to avoid allocating objects.
- return DIGITS[value % 10]
- }
-}
-
-/** Returns a linear interpolation between a and b using the scalar s. */
-private fun lerp(a: Float, b: Float, s: Float) = a + s * (b - a)
-
-/**
- * Returns the interpolation scalar (s) that satisfies the equation: `value = lerp(a, b, s)`
- *
- * If `a == b`, then this function will return 0.
- */
-private fun lerpInv(a: Float, b: Float, value: Float) = if (a != b) (value - a) / (b - a) else 0.0f
-
-internal fun getInterpolatedValue(
- startValue: Float,
- endValue: Float,
- startTime: Float,
- endTime: Float,
- currentTime: Float,
- interpolator: TimeInterpolator
-): Float {
- val progress = when {
- currentTime < startTime -> 0f
- currentTime > endTime -> 1f
- else -> interpolator.getInterpolation(lerpInv(startTime, endTime, currentTime))
- }
- return lerp(startValue, endValue, progress)
-}
-
-internal data class DigitDrawProperties(
- var shouldDraw: Boolean = false,
- var scale: Float = 0f,
- var rotation: Float = 0f,
- var opacity: Float = 0f
-)
-
-/**
- * Sets the [DigitDrawProperties] that should be used for drawing, given the specified
- * parameters.
- *
- * @param secondProgress the sub-second part of the current time, where 0 means the current second
- * has just begun, and 1 means the current second has just ended
- * @param offsetSeconds a value added to the start and end time of the animations
- * @param digitMode whether the digit is OUTGOING or INCOMING
- * @param output the [DigitDrawProperties] that will be set
- */
-internal fun getDigitDrawProperties(
- secondProgress: Float,
- offsetSeconds: Float,
- digitMode: DigitMode,
- output: DigitDrawProperties
-) {
- val startTime = DIGIT_ANIMATION_START_TIME_FRACTION[digitMode]!! + offsetSeconds
- val endTime = DIGIT_ANIMATION_END_TIME[digitMode]!! + offsetSeconds
- output.shouldDraw = if (digitMode == DigitMode.OUTGOING) {
- secondProgress < endTime
- } else {
- secondProgress >= startTime
- }
- output.scale = getInterpolatedValue(
- DIGIT_SCALE_START[digitMode]!!,
- DIGIT_SCALE_END[digitMode]!!,
- startTime,
- endTime,
- secondProgress,
- DIGIT_SCALE_INTERPOLATOR[digitMode]!!
- )
- output.rotation = getInterpolatedValue(
- DIGIT_ROTATE_START_DEGREES[digitMode]!!,
- DIGIT_ROTATE_END_DEGREES[digitMode]!!,
- startTime,
- endTime,
- secondProgress,
- DIGIT_ROTATION_INTERPOLATOR[digitMode]!!
- )
- output.opacity = getInterpolatedValue(
- DIGIT_OPACITY_START[digitMode]!!,
- DIGIT_OPACITY_END[digitMode]!!,
- startTime,
- endTime,
- secondProgress,
- DIGIT_OPACITY_INTERPOLATOR[digitMode]!!
- )
-}
-
-internal fun getTimeOffsetSeconds(digitType: DigitType): Float {
- return when (digitType) {
- DigitType.HOUR_TENS -> 5f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
- DigitType.HOUR_UNITS -> 4f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
- DigitType.MINUTE_TENS -> 3f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
- DigitType.MINUTE_UNITS -> 2f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
- DigitType.SECOND_TENS -> 1f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
- DigitType.SECOND_UNITS -> 0f
- }
-}
-
-private class DrawProperties(
- var backgroundAlpha: Float = 1f,
- var timeScale: Float = 1f,
- var secondsScale: Float = 1f
-) {
- companion object {
- val TIME_SCALE = object : FloatProperty<DrawProperties>("timeScale") {
- override fun setValue(obj: DrawProperties, value: Float) {
- obj.timeScale = value
- }
-
- override fun get(obj: DrawProperties): Float {
- return obj.timeScale
- }
- }
-
- val SECONDS_SCALE = object : FloatProperty<DrawProperties>("secondsScale") {
- override fun setValue(obj: DrawProperties, value: Float) {
- obj.secondsScale = value
- }
-
- override fun get(obj: DrawProperties): Float {
- return obj.secondsScale
- }
- }
- }
-}
-
-/** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
-internal fun multiplyColor(colorInt: Int, multiplier: Float): Int {
- val adjustedMultiplier = multiplier / 255.0f
- return colorRgb(
- Color.red(colorInt).toFloat() * adjustedMultiplier,
- Color.green(colorInt).toFloat() * adjustedMultiplier,
- Color.blue(colorInt).toFloat() * adjustedMultiplier,
- )
-}
-
-internal fun createBoundsRect(
- centerFraction: PointF,
- size: Vec2f
-): RectF {
- val halfWidth = size.x / 2.0f
- val halfHeight = size.y / 2.0f
- return RectF(
- (centerFraction.x - halfWidth),
- (centerFraction.y - halfHeight),
- (centerFraction.x + halfWidth),
- (centerFraction.y + halfHeight)
- )
-}
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.launch
/** A simple example canvas based digital watch face. */
class ExampleCanvasDigitalWatchFaceService : WatchFaceService() {
@@ -518,6 +124,8 @@
canvasComplicationFactory,
listOf(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
@@ -540,6 +148,8 @@
canvasComplicationFactory,
listOf(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
@@ -560,10 +170,13 @@
private val upperAndLowerComplicationTypes = listOf(
ComplicationType.LONG_TEXT,
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
ComplicationType.SMALL_IMAGE
)
+
// The upper and lower complicationSlots change shape depending on the complication's type.
private val upperComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder(
ComplicationID.UPPER.ordinal,
@@ -685,626 +298,1040 @@
Intent(this, ComplicationRationalActivity::class.java)
)
}
-}
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleDigitalWatchCanvasRenderer(
- surfaceHolder: SurfaceHolder,
- private val context: Context,
- private var watchFaceColorStyle: WatchFaceColorStyle,
- currentUserStyleRepository: CurrentUserStyleRepository,
- watchState: WatchState,
- private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
- private val complicationSlotsManager: ComplicationSlotsManager
-) : Renderer.CanvasRenderer(
- surfaceHolder,
- currentUserStyleRepository,
- watchState,
- CanvasType.HARDWARE,
- INTERACTIVE_UPDATE_RATE_MS,
- clearWithBackgroundTintBeforeRenderingHighlightLayer = true
-) {
- internal var oldBounds = Rect(0, 0, 0, 0)
+ @OptIn(WatchFaceExperimental::class)
+ @Suppress("Deprecation")
+ @RequiresApi(27)
+ private class ExampleDigitalWatchCanvasRenderer(
+ surfaceHolder: SurfaceHolder,
+ private val context: Context,
+ private var watchFaceColorStyle: WatchFaceColorStyle,
+ currentUserStyleRepository: CurrentUserStyleRepository,
+ watchState: WatchState,
+ private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
+ private val complicationSlotsManager: ComplicationSlotsManager
+ ) : Renderer.CanvasRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ CanvasType.HARDWARE,
+ INTERACTIVE_UPDATE_RATE_MS,
+ clearWithBackgroundTintBeforeRenderingHighlightLayer = true
+ ) {
+ internal var oldBounds = Rect(0, 0, 0, 0)
- private fun getBaseDigitPaint() = Paint().apply {
- typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
- isAntiAlias = true
- }
+ private fun getBaseDigitPaint() = Paint().apply {
+ typeface = Typeface.create(DIGITAL_TYPE_FACE, Typeface.NORMAL)
+ isAntiAlias = true
+ }
- private val digitTextHoursPaint = getBaseDigitPaint()
- private val digitTextMinutesPaint = getBaseDigitPaint()
- private val digitTextSecondsPaint = getBaseDigitPaint()
+ private val digitTextHoursPaint = getBaseDigitPaint()
+ private val digitTextMinutesPaint = getBaseDigitPaint()
+ private val digitTextSecondsPaint = getBaseDigitPaint()
- // Used for drawing the cached digits to the watchface.
- private val digitBitmapPaint = Paint().apply {
- isFilterBitmap = true
- }
+ // Used for drawing the cached digits to the watchface.
+ private val digitBitmapPaint = Paint().apply {
+ isFilterBitmap = true
+ }
- // Used for computing text sizes, not directly used for rendering.
- private var digitTextPaint = getBaseDigitPaint()
+ // Used for computing text sizes, not directly used for rendering.
+ private var digitTextPaint = getBaseDigitPaint()
- private val clockBounds = Rect()
- private val digitBounds = Rect()
- private var digitTextSize = 0f
- private var smallDigitTextSize = 0f
- private var digitHeight = 0
- private var digitWidth = 0
- private var smallDigitHeight = 0
- private var smallDigitWidth = 0
- private var digitVerticalPadding = 0
- private var gapWidth = 0f
- private val currentDigitStrings = DigitStrings()
- private val nextDigitStrings = DigitStrings()
- private val digitDrawProperties = DigitDrawProperties()
- private var drawProperties = DrawProperties()
- private var prevDrawMode = DrawMode.INTERACTIVE
+ private val clockBounds = Rect()
+ private val digitBounds = Rect()
+ private var digitTextSize = 0f
+ private var smallDigitTextSize = 0f
+ private var digitHeight = 0
+ private var digitWidth = 0
+ private var smallDigitHeight = 0
+ private var smallDigitWidth = 0
+ private var digitVerticalPadding = 0
+ private var gapWidth = 0f
+ private val currentDigitStrings = DigitStrings()
+ private val nextDigitStrings = DigitStrings()
+ private val digitDrawProperties = DigitDrawProperties()
+ private var drawProperties = DrawProperties()
+ private var prevDrawMode = DrawMode.INTERACTIVE
- // Animation played when exiting ambient mode.
- private val ambientExitAnimator = AnimatorSet().apply {
- val linearOutSlow = AnimationUtils.loadInterpolator(
- context,
- android.R.interpolator.linear_out_slow_in
- )
- playTogether(
- ObjectAnimator.ofFloat(
- drawProperties,
- DrawProperties.TIME_SCALE,
- 1.0f
- ).apply {
- duration = AMBIENT_TRANSITION_MS
- interpolator = linearOutSlow
- setAutoCancel(true)
- },
- ObjectAnimator.ofFloat(
- drawProperties,
- DrawProperties.SECONDS_SCALE,
- 1.0f
- ).apply {
- duration = AMBIENT_TRANSITION_MS
- interpolator = linearOutSlow
- setAutoCancel(true)
- }
- )
- }
+ // Animation played when exiting ambient mode.
+ private val ambientExitAnimator = AnimatorSet().apply {
+ val linearOutSlow = AnimationUtils.loadInterpolator(
+ context,
+ android.R.interpolator.linear_out_slow_in
+ )
+ playTogether(
+ ObjectAnimator.ofFloat(
+ drawProperties,
+ DrawProperties.TIME_SCALE,
+ 1.0f
+ ).apply {
+ duration = AMBIENT_TRANSITION_MS
+ interpolator = linearOutSlow
+ setAutoCancel(true)
+ },
+ ObjectAnimator.ofFloat(
+ drawProperties,
+ DrawProperties.SECONDS_SCALE,
+ 1.0f
+ ).apply {
+ duration = AMBIENT_TRANSITION_MS
+ interpolator = linearOutSlow
+ setAutoCancel(true)
+ }
+ )
+ }
- // Animation played when entering ambient mode.
- private val ambientEnterAnimator = AnimatorSet().apply {
- val fastOutLinearIn = AnimationUtils.loadInterpolator(
- context,
- android.R.interpolator.fast_out_linear_in
- )
- playTogether(
- ObjectAnimator.ofFloat(
- drawProperties,
- DrawProperties.TIME_SCALE,
- 1.0f
- ).apply {
- duration = AMBIENT_TRANSITION_MS
- interpolator = fastOutLinearIn
- setAutoCancel(true)
- },
- ObjectAnimator.ofFloat(
- drawProperties,
- DrawProperties.SECONDS_SCALE,
- 0.0f
- ).apply {
- duration = AMBIENT_TRANSITION_MS
- interpolator = fastOutLinearIn
- setAutoCancel(true)
- }
- )
- }
+ // Animation played when entering ambient mode.
+ private val ambientEnterAnimator = AnimatorSet().apply {
+ val fastOutLinearIn = AnimationUtils.loadInterpolator(
+ context,
+ android.R.interpolator.fast_out_linear_in
+ )
+ playTogether(
+ ObjectAnimator.ofFloat(
+ drawProperties,
+ DrawProperties.TIME_SCALE,
+ 1.0f
+ ).apply {
+ duration = AMBIENT_TRANSITION_MS
+ interpolator = fastOutLinearIn
+ setAutoCancel(true)
+ },
+ ObjectAnimator.ofFloat(
+ drawProperties,
+ DrawProperties.SECONDS_SCALE,
+ 0.0f
+ ).apply {
+ duration = AMBIENT_TRANSITION_MS
+ interpolator = fastOutLinearIn
+ setAutoCancel(true)
+ }
+ )
+ }
- // A mapping from digit type to cached bitmap. One bitmap is cached per digit type, and the
- // digit shown in the cached image is stored in [currentCachedDigits].
- private val digitBitmapCache = SparseArray<Bitmap>()
+ // A mapping from digit type to cached bitmap. One bitmap is cached per digit type, and the
+ // digit shown in the cached image is stored in [currentCachedDigits].
+ private val digitBitmapCache = SparseArray<Bitmap>()
- // A mapping from digit type to the digit that the cached bitmap
- // (stored in [digitBitmapCache]) displays.
- private val currentCachedDigits = SparseArray<String>()
+ // A mapping from digit type to the digit that the cached bitmap
+ // (stored in [digitBitmapCache]) displays.
+ private val currentCachedDigits = SparseArray<String>()
- private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)
+ private val coroutineScope = CoroutineScope(Dispatchers.Main.immediate)
- init {
- // Listen for style changes.
- coroutineScope.launch {
- currentUserStyleRepository.userStyle.collect { userStyle ->
- watchFaceColorStyle =
- WatchFaceColorStyle.create(
- context,
- userStyle[colorStyleSetting]!!.toString()
+ init {
+ // Listen for style changes.
+ coroutineScope.launch {
+ currentUserStyleRepository.userStyle.collect { userStyle ->
+ watchFaceColorStyle =
+ WatchFaceColorStyle.create(
+ context,
+ userStyle[colorStyleSetting]!!.toString()
+ )
+
+ watchfaceColors = WatchFaceColors(
+ Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
+ Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
+ Color.valueOf(Color.DKGRAY)
)
- watchfaceColors = WatchFaceColors(
- Color.valueOf(watchFaceColorStyle.activeStyle.primaryColor),
- Color.valueOf(watchFaceColorStyle.activeStyle.secondaryColor),
- Color.valueOf(Color.DKGRAY)
- )
+ // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
+ // of the styles are defined in XML so we need to replace the complication's
+ // drawables.
+ for ((_, complication) in complicationSlotsManager.complicationSlots) {
+ (complication.renderer as CanvasComplicationDrawable).drawable =
+ watchFaceColorStyle.getDrawable(context)!!
+ }
- // Apply the userStyle to the complicationSlots. ComplicationDrawables for each
- // of the styles are defined in XML so we need to replace the complication's
- // drawables.
- for ((_, complication) in complicationSlotsManager.complicationSlots) {
- (complication.renderer as CanvasComplicationDrawable).drawable =
- watchFaceColorStyle.getDrawable(context)!!
+ clearDigitBitmapCache()
}
+ }
+ // Listen for ambient state changes.
+ coroutineScope.launch {
+ watchState.isAmbient.collect {
+ if (it!!) {
+ ambientEnterAnimator.start()
+ } else {
+ ambientExitAnimator.start()
+ }
+
+ // Trigger recomputation of bounds.
+ oldBounds.set(0, 0, 0, 0)
+ val antiAlias = !(it && watchState.hasLowBitAmbient)
+ digitTextHoursPaint.isAntiAlias = antiAlias
+ digitTextMinutesPaint.isAntiAlias = antiAlias
+ digitTextSecondsPaint.isAntiAlias = antiAlias
+ }
+ }
+ }
+
+ override fun shouldAnimate(): Boolean {
+ // Make sure we keep animating while ambientEnterAnimator is running.
+ return ambientEnterAnimator.isRunning || super.shouldAnimate()
+ }
+
+ private fun applyColorStyleAndDrawMode(drawMode: DrawMode) {
+ digitTextHoursPaint.color = when (drawMode) {
+ DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
+ DrawMode.LOW_BATTERY_INTERACTIVE ->
+ multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
+
+ DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
+ DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
+ }
+
+ digitTextMinutesPaint.color = when (drawMode) {
+ DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
+ DrawMode.LOW_BATTERY_INTERACTIVE ->
+ multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
+
+ DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
+ DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
+ }
+
+ digitTextSecondsPaint.color = when (drawMode) {
+ DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.secondaryColor
+ DrawMode.LOW_BATTERY_INTERACTIVE ->
+ multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.6f)
+
+ DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.8f)
+ DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.secondaryColor
+ }
+
+ if (prevDrawMode != drawMode) {
+ prevDrawMode = drawMode
clearDigitBitmapCache()
}
}
- // Listen for ambient state changes.
- coroutineScope.launch {
- watchState.isAmbient.collect {
- if (it!!) {
- ambientEnterAnimator.start()
- } else {
- ambientExitAnimator.start()
- }
+ override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
+ recalculateBoundsIfChanged(bounds, zonedDateTime)
- // Trigger recomputation of bounds.
- oldBounds.set(0, 0, 0, 0)
- val antiAlias = !(it && watchState.hasLowBitAmbient)
- digitTextHoursPaint.isAntiAlias = antiAlias
- digitTextMinutesPaint.isAntiAlias = antiAlias
- digitTextSecondsPaint.isAntiAlias = antiAlias
+ applyColorStyleAndDrawMode(renderParameters.drawMode)
+
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+ drawBackground(canvas)
}
- }
- }
- override fun shouldAnimate(): Boolean {
- // Make sure we keep animating while ambientEnterAnimator is running.
- return ambientEnterAnimator.isRunning || super.shouldAnimate()
- }
+ drawComplications(canvas, zonedDateTime)
- private fun applyColorStyleAndDrawMode(drawMode: DrawMode) {
- digitTextHoursPaint.color = when (drawMode) {
- DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
- DrawMode.LOW_BATTERY_INTERACTIVE ->
- multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
- DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
- DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
- }
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+ val is24Hour: Boolean = DateFormat.is24HourFormat(context)
- digitTextMinutesPaint.color = when (drawMode) {
- DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.primaryColor
- DrawMode.LOW_BATTERY_INTERACTIVE ->
- multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.6f)
- DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.primaryColor, 0.8f)
- DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.primaryColor
- }
+ currentDigitStrings.set(zonedDateTime, is24Hour)
+ nextDigitStrings.set(zonedDateTime.plusSeconds(1), is24Hour)
- digitTextSecondsPaint.color = when (drawMode) {
- DrawMode.INTERACTIVE -> watchFaceColorStyle.activeStyle.secondaryColor
- DrawMode.LOW_BATTERY_INTERACTIVE ->
- multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.6f)
- DrawMode.MUTE -> multiplyColor(watchFaceColorStyle.activeStyle.secondaryColor, 0.8f)
- DrawMode.AMBIENT -> watchFaceColorStyle.ambientStyle.secondaryColor
- }
+ val secondProgress = (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
- if (prevDrawMode != drawMode) {
- prevDrawMode = drawMode
- clearDigitBitmapCache()
- }
- }
-
- override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
- recalculateBoundsIfChanged(bounds, zonedDateTime)
-
- applyColorStyleAndDrawMode(renderParameters.drawMode)
-
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
- drawBackground(canvas)
- }
-
- drawComplications(canvas, zonedDateTime)
-
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
- val is24Hour: Boolean = DateFormat.is24HourFormat(context)
-
- currentDigitStrings.set(zonedDateTime, is24Hour)
- nextDigitStrings.set(zonedDateTime.plusSeconds(1), is24Hour)
-
- val secondProgress = (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
- val animationStartFraction = DIGIT_ANIMATION_START_TIME_FRACTION[DigitMode.OUTGOING]!!
- this.interactiveDrawModeUpdateDelayMillis =
- if (secondProgress < animationStartFraction &&
- !ambientEnterAnimator.isRunning
- ) {
- // The seconds only animate part of the time so we can sleep until the seconds next need to
- // animate, which improves battery life.
- max(
- INTERACTIVE_UPDATE_RATE_MS,
- ((animationStartFraction - secondProgress) * 1000f).toLong()
- )
- } else {
- INTERACTIVE_UPDATE_RATE_MS
- }
-
- // Move the left position to the left if there are fewer than two hour digits, to
- // ensure it is centered. If the clock is in transition from one to two hour digits or
- // vice versa, interpolate to animate the clock's position.
- // Move the left position to the left if there are fewer than two hour digits, to ensure
- // it is centered. If the clock is in transition from one to two hour digits or
- // vice versa, interpolate to animate the clock's position.
- val centeringAdjustment = (
- getInterpolatedValue(
- (2 - currentDigitStrings.getNumberOfHoursDigits()).toFloat(),
- (2 - nextDigitStrings.getNumberOfHoursDigits()).toFloat(),
- POSITION_ANIMATION_START_TIME,
- POSITION_ANIMATION_END_TIME,
- secondProgress,
- CENTERING_ADJUSTMENT_INTERPOLATOR
- ) *
- digitWidth
- )
-
- // This total width assumes two hours digits.
- val totalWidth = 2f * digitWidth + gapWidth + 2f * smallDigitWidth
- val left = clockBounds.exactCenterX() - 0.5f * (totalWidth + centeringAdjustment)
- val top = clockBounds.exactCenterY() - 0.5f * digitHeight
-
- val wholeTimeSaveCount = canvas.save()
- try {
- canvas.scale(
- drawProperties.timeScale, drawProperties.timeScale,
- clockBounds.exactCenterX(), clockBounds.exactCenterY()
- )
-
- // Draw hours.
- drawDigit(
- canvas,
- left,
- top,
- currentDigitStrings,
- nextDigitStrings,
- DigitType.HOUR_TENS,
- secondProgress
- )
- drawDigit(
- canvas,
- left + digitWidth,
- top,
- currentDigitStrings,
- nextDigitStrings,
- DigitType.HOUR_UNITS,
- secondProgress
- )
-
- // Draw minutes.
- val minutesLeft = left + digitWidth * 2.0f + gapWidth
- drawDigit(
- canvas,
- minutesLeft,
- top,
- currentDigitStrings,
- nextDigitStrings,
- DigitType.MINUTE_TENS,
- secondProgress
- )
- drawDigit(
- canvas,
- minutesLeft + smallDigitWidth,
- top,
- currentDigitStrings,
- nextDigitStrings,
- DigitType.MINUTE_UNITS,
- secondProgress
- )
-
- // Scale the seconds if they're not fully showing, in and out of ambient for
- // example.
- val scaleSeconds = drawProperties.secondsScale < 1.0f
- if (drawProperties.secondsScale > 0f &&
- (renderParameters.drawMode != DrawMode.AMBIENT || scaleSeconds)
- ) {
- val restoreCount = canvas.save()
- if (scaleSeconds) {
- // Scale the canvas around the center of the seconds bounds.
- canvas.scale(
- drawProperties.secondsScale,
- drawProperties.secondsScale,
- minutesLeft + smallDigitWidth,
- top + digitHeight - smallDigitHeight / 2
+ val animationStartFraction =
+ DIGIT_ANIMATION_START_TIME_FRACTION[DigitMode.OUTGOING]!!
+ this.interactiveDrawModeUpdateDelayMillis =
+ if (secondProgress < animationStartFraction &&
+ !ambientEnterAnimator.isRunning
+ ) {
+ // The seconds only animate part of the time so we can sleep until the seconds next need to
+ // animate, which improves battery life.
+ max(
+ INTERACTIVE_UPDATE_RATE_MS,
+ ((animationStartFraction - secondProgress) * 1000f).toLong()
)
+ } else {
+ INTERACTIVE_UPDATE_RATE_MS
}
- // Draw seconds.
- val secondsTop = top + digitHeight - smallDigitHeight
+ // Move the left position to the left if there are fewer than two hour digits, to
+ // ensure it is centered. If the clock is in transition from one to two hour digits or
+ // vice versa, interpolate to animate the clock's position.
+ // Move the left position to the left if there are fewer than two hour digits, to ensure
+ // it is centered. If the clock is in transition from one to two hour digits or
+ // vice versa, interpolate to animate the clock's position.
+ val centeringAdjustment = (
+ getInterpolatedValue(
+ (2 - currentDigitStrings.getNumberOfHoursDigits()).toFloat(),
+ (2 - nextDigitStrings.getNumberOfHoursDigits()).toFloat(),
+ POSITION_ANIMATION_START_TIME,
+ POSITION_ANIMATION_END_TIME,
+ secondProgress,
+ CENTERING_ADJUSTMENT_INTERPOLATOR
+ ) *
+ digitWidth
+ )
+
+ // This total width assumes two hours digits.
+ val totalWidth = 2f * digitWidth + gapWidth + 2f * smallDigitWidth
+ val left = clockBounds.exactCenterX() - 0.5f * (totalWidth + centeringAdjustment)
+ val top = clockBounds.exactCenterY() - 0.5f * digitHeight
+
+ val wholeTimeSaveCount = canvas.save()
+ try {
+ canvas.scale(
+ drawProperties.timeScale, drawProperties.timeScale,
+ clockBounds.exactCenterX(), clockBounds.exactCenterY()
+ )
+
+ // Draw hours.
+ drawDigit(
+ canvas,
+ left,
+ top,
+ currentDigitStrings,
+ nextDigitStrings,
+ DigitType.HOUR_TENS,
+ secondProgress
+ )
+ drawDigit(
+ canvas,
+ left + digitWidth,
+ top,
+ currentDigitStrings,
+ nextDigitStrings,
+ DigitType.HOUR_UNITS,
+ secondProgress
+ )
+
+ // Draw minutes.
+ val minutesLeft = left + digitWidth * 2.0f + gapWidth
drawDigit(
canvas,
minutesLeft,
- secondsTop,
+ top,
currentDigitStrings,
nextDigitStrings,
- DigitType.SECOND_TENS,
+ DigitType.MINUTE_TENS,
secondProgress
)
drawDigit(
canvas,
minutesLeft + smallDigitWidth,
- secondsTop,
+ top,
currentDigitStrings,
nextDigitStrings,
- DigitType.SECOND_UNITS,
+ DigitType.MINUTE_UNITS,
secondProgress
)
- canvas.restoreToCount(restoreCount)
- }
- } finally {
- canvas.restoreToCount(wholeTimeSaveCount)
- }
- }
- }
+ // Scale the seconds if they're not fully showing, in and out of ambient for
+ // example.
+ val scaleSeconds = drawProperties.secondsScale < 1.0f
+ if (drawProperties.secondsScale > 0f &&
+ (renderParameters.drawMode != DrawMode.AMBIENT || scaleSeconds)
+ ) {
+ val restoreCount = canvas.save()
+ if (scaleSeconds) {
+ // Scale the canvas around the center of the seconds bounds.
+ canvas.scale(
+ drawProperties.secondsScale,
+ drawProperties.secondsScale,
+ minutesLeft + smallDigitWidth,
+ top + digitHeight - smallDigitHeight / 2
+ )
+ }
- override fun renderHighlightLayer(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
- drawComplicationHighlights(canvas, zonedDateTime)
- }
+ // Draw seconds.
+ val secondsTop = top + digitHeight - smallDigitHeight
+ drawDigit(
+ canvas,
+ minutesLeft,
+ secondsTop,
+ currentDigitStrings,
+ nextDigitStrings,
+ DigitType.SECOND_TENS,
+ secondProgress
+ )
+ drawDigit(
+ canvas,
+ minutesLeft + smallDigitWidth,
+ secondsTop,
+ currentDigitStrings,
+ nextDigitStrings,
+ DigitType.SECOND_UNITS,
+ secondProgress
+ )
- override fun getMainClockElementBounds() = clockBounds
-
- private fun recalculateBoundsIfChanged(bounds: Rect, zonedDateTime: ZonedDateTime) {
- if (oldBounds == bounds) {
- return
- }
-
- oldBounds.set(bounds)
- calculateClockBound(bounds, zonedDateTime)
- }
-
- private fun calculateClockBound(bounds: Rect, zonedDateTime: ZonedDateTime) {
- val hasVerticalComplication =
- VERTICAL_COMPLICATION_IDS.any {
- complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
- }
- val hasHorizontalComplication =
- HORIZONTAL_COMPLICATION_IDS.any {
- complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
- }
-
- val marginX = if (hasHorizontalComplication) {
- (MARGIN_FRACTION_WITH_COMPLICATION.x * bounds.width().toFloat()).toInt()
- } else {
- (MARGIN_FRACTION_WITHOUT_COMPLICATION.x * bounds.width().toFloat()).toInt()
- }
-
- val marginY = if (hasVerticalComplication) {
- (MARGIN_FRACTION_WITH_COMPLICATION.y * bounds.height().toFloat()).toInt()
- } else {
- (MARGIN_FRACTION_WITHOUT_COMPLICATION.y * bounds.height().toFloat()).toInt()
- }
-
- clockBounds.set(
- bounds.left + marginX,
- bounds.top + marginY,
- bounds.right - marginX,
- bounds.bottom - marginY
- )
-
- recalculateTextSize()
- }
-
- private fun recalculateTextSize() {
- // Determine the font size to fit by measuring the text for a sample size and compare to
- // the size. That assume the height of the text scales linearly.
- val sampleTextSize = clockBounds.height().toFloat()
- digitTextPaint.setTextSize(sampleTextSize)
- digitTextPaint.getTextBounds(ALL_DIGITS, 0, ALL_DIGITS.length, digitBounds)
- var textSize = clockBounds.height() * sampleTextSize / digitBounds.height().toFloat()
- val textRatio = textSize / clockBounds.height().toFloat()
-
- // Remain a top and bottom padding.
- textSize *= (1f - 2f * DIGIT_PADDING_FRACTION)
-
- // Calculate the total width fraction base on the text height.
- val totalWidthFraction: Float =
- (2f * DIGIT_WIDTH_FRACTION) + (DIGIT_WIDTH_FRACTION * GAP_WIDTH_FRACTION) +
- (2f * SMALL_DIGIT_SIZE_FRACTION * SMALL_DIGIT_WIDTH_FRACTION)
-
- textSize = min(textSize, (clockBounds.width().toFloat() / totalWidthFraction) * textRatio)
-
- setDigitTextSize(textSize)
- }
-
- private fun setDigitTextSize(textSize: Float) {
- clearDigitBitmapCache()
-
- digitTextSize = textSize
- digitTextPaint.textSize = digitTextSize
- digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
- digitVerticalPadding = (digitBounds.height() * DIGIT_PADDING_FRACTION).toInt()
- digitWidth = (digitBounds.height() * DIGIT_WIDTH_FRACTION).toInt()
- digitHeight = digitBounds.height() + 2 * digitVerticalPadding
-
- smallDigitTextSize = textSize * SMALL_DIGIT_SIZE_FRACTION
- digitTextPaint.setTextSize(smallDigitTextSize)
- digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
- smallDigitHeight = digitBounds.height() + 2 * digitVerticalPadding
- smallDigitWidth = (digitBounds.height() * SMALL_DIGIT_WIDTH_FRACTION).toInt()
-
- gapWidth = digitHeight * GAP_WIDTH_FRACTION
-
- digitTextHoursPaint.textSize = digitTextSize
- digitTextMinutesPaint.textSize = smallDigitTextSize
- digitTextSecondsPaint.textSize = smallDigitTextSize
- }
-
- private fun drawBackground(canvas: Canvas) {
- val backgroundColor = if (renderParameters.drawMode == DrawMode.AMBIENT) {
- watchFaceColorStyle.ambientStyle.backgroundColor
- } else {
- watchFaceColorStyle.activeStyle.backgroundColor
- }
- canvas.drawColor(
- getRGBColor(
- backgroundColor,
- drawProperties.backgroundAlpha,
- Color.BLACK
- )
- )
- }
-
- private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) {
- // First, draw the background complication if not in ambient mode
- if (renderParameters.drawMode != DrawMode.AMBIENT) {
- complicationSlotsManager[ComplicationID.BACKGROUND.ordinal]?.let {
- if (it.complicationData.value.type != ComplicationType.NO_DATA) {
- it.render(
- canvas,
- zonedDateTime,
- renderParameters
- )
+ canvas.restoreToCount(restoreCount)
+ }
+ } finally {
+ canvas.restoreToCount(wholeTimeSaveCount)
}
}
}
- for (i in FOREGROUND_COMPLICATION_IDS) {
- val complication = complicationSlotsManager[i] as ComplicationSlot
- complication.render(canvas, zonedDateTime, renderParameters)
+
+ override fun renderHighlightLayer(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime
+ ) {
+ drawComplicationHighlights(canvas, zonedDateTime)
+ }
+
+ override fun getMainClockElementBounds() = clockBounds
+
+ private fun recalculateBoundsIfChanged(bounds: Rect, zonedDateTime: ZonedDateTime) {
+ if (oldBounds == bounds) {
+ return
+ }
+
+ oldBounds.set(bounds)
+ calculateClockBound(bounds, zonedDateTime)
+ }
+
+ private fun calculateClockBound(bounds: Rect, zonedDateTime: ZonedDateTime) {
+ val hasVerticalComplication =
+ VERTICAL_COMPLICATION_IDS.any {
+ complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
+ }
+ val hasHorizontalComplication =
+ HORIZONTAL_COMPLICATION_IDS.any {
+ complicationSlotsManager[it]!!.isActiveAt(zonedDateTime.toInstant())
+ }
+
+ val marginX = if (hasHorizontalComplication) {
+ (MARGIN_FRACTION_WITH_COMPLICATION.x * bounds.width().toFloat()).toInt()
+ } else {
+ (MARGIN_FRACTION_WITHOUT_COMPLICATION.x * bounds.width().toFloat()).toInt()
+ }
+
+ val marginY = if (hasVerticalComplication) {
+ (MARGIN_FRACTION_WITH_COMPLICATION.y * bounds.height().toFloat()).toInt()
+ } else {
+ (MARGIN_FRACTION_WITHOUT_COMPLICATION.y * bounds.height().toFloat()).toInt()
+ }
+
+ clockBounds.set(
+ bounds.left + marginX,
+ bounds.top + marginY,
+ bounds.right - marginX,
+ bounds.bottom - marginY
+ )
+
+ recalculateTextSize()
+ }
+
+ private fun recalculateTextSize() {
+ // Determine the font size to fit by measuring the text for a sample size and compare to
+ // the size. That assume the height of the text scales linearly.
+ val sampleTextSize = clockBounds.height().toFloat()
+ digitTextPaint.setTextSize(sampleTextSize)
+ digitTextPaint.getTextBounds(ALL_DIGITS, 0, ALL_DIGITS.length, digitBounds)
+ var textSize = clockBounds.height() * sampleTextSize / digitBounds.height().toFloat()
+ val textRatio = textSize / clockBounds.height().toFloat()
+
+ // Remain a top and bottom padding.
+ textSize *= (1f - 2f * DIGIT_PADDING_FRACTION)
+
+ // Calculate the total width fraction base on the text height.
+ val totalWidthFraction: Float =
+ (2f * DIGIT_WIDTH_FRACTION) + (DIGIT_WIDTH_FRACTION * GAP_WIDTH_FRACTION) +
+ (2f * SMALL_DIGIT_SIZE_FRACTION * SMALL_DIGIT_WIDTH_FRACTION)
+
+ textSize =
+ min(textSize, (clockBounds.width().toFloat() / totalWidthFraction) * textRatio)
+
+ setDigitTextSize(textSize)
+ }
+
+ private fun setDigitTextSize(textSize: Float) {
+ clearDigitBitmapCache()
+
+ digitTextSize = textSize
+ digitTextPaint.textSize = digitTextSize
+ digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
+ digitVerticalPadding = (digitBounds.height() * DIGIT_PADDING_FRACTION).toInt()
+ digitWidth = (digitBounds.height() * DIGIT_WIDTH_FRACTION).toInt()
+ digitHeight = digitBounds.height() + 2 * digitVerticalPadding
+
+ smallDigitTextSize = textSize * SMALL_DIGIT_SIZE_FRACTION
+ digitTextPaint.setTextSize(smallDigitTextSize)
+ digitTextPaint.getTextBounds(ALL_DIGITS, 0, 1, digitBounds)
+ smallDigitHeight = digitBounds.height() + 2 * digitVerticalPadding
+ smallDigitWidth = (digitBounds.height() * SMALL_DIGIT_WIDTH_FRACTION).toInt()
+
+ gapWidth = digitHeight * GAP_WIDTH_FRACTION
+
+ digitTextHoursPaint.textSize = digitTextSize
+ digitTextMinutesPaint.textSize = smallDigitTextSize
+ digitTextSecondsPaint.textSize = smallDigitTextSize
+ }
+
+ private fun drawBackground(canvas: Canvas) {
+ val backgroundColor = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+ watchFaceColorStyle.ambientStyle.backgroundColor
+ } else {
+ watchFaceColorStyle.activeStyle.backgroundColor
+ }
+ canvas.drawColor(
+ getRGBColor(
+ backgroundColor,
+ drawProperties.backgroundAlpha,
+ Color.BLACK
+ )
+ )
+ }
+
+ private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) {
+ // First, draw the background complication if not in ambient mode
+ if (renderParameters.drawMode != DrawMode.AMBIENT) {
+ complicationSlotsManager[ComplicationID.BACKGROUND.ordinal]?.let {
+ if (it.complicationData.value.type != ComplicationType.NO_DATA) {
+ it.render(
+ canvas,
+ zonedDateTime,
+ renderParameters
+ )
+ }
+ }
+ }
+ for (i in FOREGROUND_COMPLICATION_IDS) {
+ val complication = complicationSlotsManager[i] as ComplicationSlot
+ complication.render(canvas, zonedDateTime, renderParameters)
+ }
+ }
+
+ private fun drawComplicationHighlights(canvas: Canvas, zonedDateTime: ZonedDateTime) {
+ for (i in FOREGROUND_COMPLICATION_IDS) {
+ val complication = complicationSlotsManager[i] as ComplicationSlot
+ complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+ }
+ }
+
+ private fun clearDigitBitmapCache() {
+ currentCachedDigits.clear()
+ digitBitmapCache.clear()
+ }
+
+ private fun drawDigit(
+ canvas: Canvas,
+ left: Float,
+ top: Float,
+ currentDigitStrings: DigitStrings,
+ nextDigitStrings: DigitStrings,
+ digitType: DigitType,
+ secondProgress: Float
+ ) {
+ val currentDigit = currentDigitStrings.get(digitType)
+ val nextDigit = nextDigitStrings.get(digitType)
+
+ // Draw the current digit bitmap.
+ if (currentDigit != nextDigit) {
+ drawDigitWithAnimation(
+ canvas,
+ left,
+ top,
+ secondProgress,
+ currentDigit,
+ digitType,
+ DigitMode.OUTGOING
+ )
+ drawDigitWithAnimation(
+ canvas,
+ left,
+ top,
+ secondProgress,
+ nextDigit,
+ digitType,
+ DigitMode.INCOMING
+ )
+ } else {
+ canvas.drawBitmap(
+ getDigitBitmap(currentDigit, digitType),
+ left,
+ top,
+ digitBitmapPaint
+ )
+ }
+ }
+
+ private fun drawDigitWithAnimation(
+ canvas: Canvas,
+ left: Float,
+ top: Float,
+ secondProgress: Float,
+ digit: String,
+ digitType: DigitType,
+ digitMode: DigitMode
+ ) {
+ getDigitDrawProperties(
+ secondProgress, getTimeOffsetSeconds(digitType), digitMode, digitDrawProperties
+ )
+
+ if (!digitDrawProperties.shouldDraw) {
+ return
+ }
+
+ val bitmap = getDigitBitmap(digit, digitType)
+ val centerX = left + bitmap.width / 2f
+ val centerY = top + bitmap.height / 2f
+ val restoreCount = canvas.save()
+ canvas.scale(digitDrawProperties.scale, digitDrawProperties.scale, centerX, centerY)
+ canvas.rotate(digitDrawProperties.rotation, centerX, centerY)
+ val bitmapPaint = digitBitmapPaint
+ bitmapPaint.alpha = (digitDrawProperties.opacity * 255.0f).toInt()
+ canvas.drawBitmap(bitmap, left, top, bitmapPaint)
+ bitmapPaint.alpha = 255
+ canvas.restoreToCount(restoreCount)
+ }
+
+ private fun createBitmap(
+ width: Int,
+ height: Int,
+ digitType: DigitType
+ ): Bitmap {
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ digitBitmapCache.put(digitType.ordinal, bitmap)
+ return bitmap
+ }
+
+ /**
+ * Returns a {@link Bitmap} representing the given {@code digit}, suitable for use as the
+ * given {@code digitType}.
+ */
+ private fun getDigitBitmap(digit: String, digitType: DigitType): Bitmap {
+ val width: Int
+ val height: Int
+ val paint: Paint
+ when (digitType) {
+ DigitType.HOUR_TENS, DigitType.HOUR_UNITS -> {
+ width = digitWidth
+ height = digitHeight
+ paint = digitTextHoursPaint
+ }
+
+ DigitType.MINUTE_TENS, DigitType.MINUTE_UNITS -> {
+ width = smallDigitWidth
+ height = smallDigitHeight
+ paint = digitTextMinutesPaint
+ }
+
+ DigitType.SECOND_TENS, DigitType.SECOND_UNITS -> {
+ width = smallDigitWidth
+ height = smallDigitHeight
+ paint = digitTextSecondsPaint
+ }
+ }
+
+ val bitmap =
+ digitBitmapCache.get(digitType.ordinal) ?: createBitmap(width, height, digitType)
+ if (digit != currentCachedDigits.get(digitType.ordinal)) {
+ currentCachedDigits.put(digitType.ordinal, digit)
+
+ bitmap.eraseColor(Color.TRANSPARENT)
+ val canvas = Canvas(bitmap)
+ val textWidth = paint.measureText(digit)
+
+ // We use the same vertical padding here for all types and sizes of digit so that
+ // their tops and bottoms can be aligned.
+ canvas.drawText(
+ digit,
+ (width - textWidth) / 2,
+ height.toFloat() - digitVerticalPadding,
+ paint
+ )
+ }
+ return bitmap
}
}
- private fun drawComplicationHighlights(canvas: Canvas, zonedDateTime: ZonedDateTime) {
- for (i in FOREGROUND_COMPLICATION_IDS) {
- val complication = complicationSlotsManager[i] as ComplicationSlot
- complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+ private data class Vec2f(val x: Float, val y: Float)
+
+ private enum class ComplicationID {
+ UPPER,
+ RIGHT,
+ LOWER,
+ LEFT,
+ BACKGROUND
+ }
+
+ // Changing digits are animated. This enum is used to label the start and end animation parameters.
+ private enum class DigitMode {
+ OUTGOING,
+ INCOMING
+ }
+
+ private enum class DigitType {
+ HOUR_TENS,
+ HOUR_UNITS,
+ MINUTE_TENS,
+ MINUTE_UNITS,
+ SECOND_TENS,
+ SECOND_UNITS
+ }
+
+ /** A class to provide string representations of each of the digits of a given time. */
+ private class DigitStrings {
+ private var hourTens = ""
+ private var hourUnits = ""
+ private var minuteTens = ""
+ private var minuteUnits = ""
+ private var secondTens = ""
+ private var secondUnits = ""
+
+ /** Sets the time represented by this instance. */
+ fun set(zonedDateTime: ZonedDateTime, is24Hour: Boolean) {
+ if (is24Hour) {
+ val hourValue = zonedDateTime.hour
+ hourTens = getTensDigitString(hourValue, true)
+ hourUnits = getUnitsDigitString(hourValue)
+ } else {
+ var hourValue = zonedDateTime.hour % 12
+ // We should show 12 for noon and midnight.
+ if (hourValue == 0) {
+ hourValue = 12
+ }
+ hourTens = getTensDigitString(hourValue, false)
+ hourUnits = getUnitsDigitString(hourValue)
+ }
+
+ val minuteValue = zonedDateTime.minute
+ minuteTens = getTensDigitString(minuteValue, true)
+ minuteUnits = getUnitsDigitString(minuteValue)
+ val secondsValue = zonedDateTime.second
+ secondTens = getTensDigitString(secondsValue, true)
+ secondUnits = getUnitsDigitString(secondsValue)
+ }
+
+ /** Returns a string representing the specified digit of the time represented by this object. */
+ fun get(digitType: DigitType): String {
+ return when (digitType) {
+ DigitType.HOUR_TENS -> hourTens
+ DigitType.HOUR_UNITS -> hourUnits
+ DigitType.MINUTE_TENS -> minuteTens
+ DigitType.MINUTE_UNITS -> minuteUnits
+ DigitType.SECOND_TENS -> secondTens
+ DigitType.SECOND_UNITS -> secondUnits
+ }
+ }
+
+ /**
+ * Returns the number of hour digits in this object. If the representation is 24-hour, this will
+ * always return 2. If 12-hour, this will return 1 or 2.
+ */
+ fun getNumberOfHoursDigits(): Int {
+ return if (hourTens == "") 1 else 2
+ }
+
+ /**
+ * Returns a {@link String} representing the tens digit of the provided non-negative {@code
+ * value}. If {@code padWithZeroes} is true, returns zero if {@code value} < 10. If {@code
+ * padWithZeroes} is false, returns an empty string if {@code value} < 10.
+ */
+ private fun getTensDigitString(value: Int, padWithZeroes: Boolean): String {
+ if (value < 10 && !padWithZeroes) {
+ return ""
+ }
+ // We don't use toString() because during draw calls we don't want to avoid allocating objects.
+ return DIGITS[(value / 10) % 10]
+ }
+
+ /**
+ * Returns a {@link String} representing the units digit of the provided non-negative {@code
+ * value}.
+ */
+ private fun getUnitsDigitString(value: Int): String {
+ // We don't use toString() because during draw calls we don't want to avoid allocating objects.
+ return DIGITS[value % 10]
}
}
- private fun clearDigitBitmapCache() {
- currentCachedDigits.clear()
- digitBitmapCache.clear()
- }
+ private data class DigitDrawProperties(
+ var shouldDraw: Boolean = false,
+ var scale: Float = 0f,
+ var rotation: Float = 0f,
+ var opacity: Float = 0f
+ )
- private fun drawDigit(
- canvas: Canvas,
- left: Float,
- top: Float,
- currentDigitStrings: DigitStrings,
- nextDigitStrings: DigitStrings,
- digitType: DigitType,
- secondProgress: Float
+ private class DrawProperties(
+ var backgroundAlpha: Float = 1f,
+ var timeScale: Float = 1f,
+ var secondsScale: Float = 1f
) {
- val currentDigit = currentDigitStrings.get(digitType)
- val nextDigit = nextDigitStrings.get(digitType)
+ companion object {
+ val TIME_SCALE = object : FloatProperty<DrawProperties>("timeScale") {
+ override fun setValue(obj: DrawProperties, value: Float) {
+ obj.timeScale = value
+ }
- // Draw the current digit bitmap.
- if (currentDigit != nextDigit) {
- drawDigitWithAnimation(
- canvas,
- left,
- top,
- secondProgress,
- currentDigit,
- digitType,
- DigitMode.OUTGOING
- )
- drawDigitWithAnimation(
- canvas,
- left,
- top,
- secondProgress,
- nextDigit,
- digitType,
- DigitMode.INCOMING
- )
- } else {
- canvas.drawBitmap(
- getDigitBitmap(currentDigit, digitType),
- left,
- top,
- digitBitmapPaint
- )
+ override fun get(obj: DrawProperties): Float {
+ return obj.timeScale
+ }
+ }
+
+ val SECONDS_SCALE = object : FloatProperty<DrawProperties>("secondsScale") {
+ override fun setValue(obj: DrawProperties, value: Float) {
+ obj.secondsScale = value
+ }
+
+ override fun get(obj: DrawProperties): Float {
+ return obj.secondsScale
+ }
+ }
}
}
- private fun drawDigitWithAnimation(
- canvas: Canvas,
- left: Float,
- top: Float,
- secondProgress: Float,
- digit: String,
- digitType: DigitType,
- digitMode: DigitMode
- ) {
- getDigitDrawProperties(
- secondProgress, getTimeOffsetSeconds(digitType), digitMode, digitDrawProperties
+ companion object {
+ private const val COLOR_STYLE_SETTING = "color_style_setting"
+ private const val RED_STYLE = "red_style"
+ private const val GREEN_STYLE = "green_style"
+ private const val BLUE_STYLE = "blue_style"
+
+ // Render at approximately 60fps in interactive mode.
+ private const val INTERACTIVE_UPDATE_RATE_MS = 16L
+
+ // Constants for the size of complication.
+ private val CIRCLE_COMPLICATION_DIAMETER_FRACTION = Vec2f(0.252f, 0.252f)
+ private val ROUND_RECT_COMPLICATION_SIZE_FRACTION = Vec2f(0.645f, 0.168f)
+
+ // Constants for the upper complication location.
+ private val UPPER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
+ private val UPPER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.21f)
+
+ // Constants for the lower complication location.
+ private val LOWER_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
+ private val LOWER_ROUND_RECT_COMPLICATION_CENTER_FRACTION = PointF(0.5f, 0.79f)
+
+ // Constants for the left complication location.
+ private val LEFT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.177f, 0.5f)
+
+ // Constants for the right complication location.
+ private val RIGHT_CIRCLE_COMPLICATION_CENTER_FRACTION = PointF(0.823f, 0.5f)
+
+ // Constants for the clock digits' position, based on the height and width of given bounds.
+ private val MARGIN_FRACTION_WITHOUT_COMPLICATION = Vec2f(0.2f, 0.2f)
+ private val MARGIN_FRACTION_WITH_COMPLICATION = Vec2f(0.4f, 0.4f)
+
+ private val VERTICAL_COMPLICATION_IDS = arrayOf(
+ ComplicationID.UPPER.ordinal,
+ ComplicationID.LOWER.ordinal
+ )
+ private val HORIZONTAL_COMPLICATION_IDS = arrayOf(
+ ComplicationID.LEFT.ordinal,
+ ComplicationID.RIGHT.ordinal
+ )
+ private val FOREGROUND_COMPLICATION_IDS = arrayOf(
+ ComplicationID.UPPER.ordinal, ComplicationID.RIGHT.ordinal,
+ ComplicationID.LOWER.ordinal, ComplicationID.LEFT.ordinal
)
- if (!digitDrawProperties.shouldDraw) {
- return
- }
+ // The name of the font used for drawing the text in the digit watch face.
+ private const val DIGITAL_TYPE_FACE = "sans-serif-condensed-light"
- val bitmap = getDigitBitmap(digit, digitType)
- val centerX = left + bitmap.width / 2f
- val centerY = top + bitmap.height / 2f
- val restoreCount = canvas.save()
- canvas.scale(digitDrawProperties.scale, digitDrawProperties.scale, centerX, centerY)
- canvas.rotate(digitDrawProperties.rotation, centerX, centerY)
- val bitmapPaint = digitBitmapPaint
- bitmapPaint.alpha = (digitDrawProperties.opacity * 255.0f).toInt()
- canvas.drawBitmap(bitmap, left, top, bitmapPaint)
- bitmapPaint.alpha = 255
- canvas.restoreToCount(restoreCount)
- }
+ // The width of the large digit bitmaps, as a fraction of their height.
+ private const val DIGIT_WIDTH_FRACTION = 0.65f
- private fun createBitmap(
- width: Int,
- height: Int,
- digitType: DigitType
- ): Bitmap {
- val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
- digitBitmapCache.put(digitType.ordinal, bitmap)
- return bitmap
- }
+ // The height of the small digits (used for minutes and seconds), given as a fraction of the height
+ // of the large digits.
+ private const val SMALL_DIGIT_SIZE_FRACTION = 0.45f
- /**
- * Returns a {@link Bitmap} representing the given {@code digit}, suitable for use as the
- * given {@code digitType}.
- */
- private fun getDigitBitmap(digit: String, digitType: DigitType): Bitmap {
- val width: Int
- val height: Int
- val paint: Paint
- when (digitType) {
- DigitType.HOUR_TENS, DigitType.HOUR_UNITS -> {
- width = digitWidth
- height = digitHeight
- paint = digitTextHoursPaint
- }
- DigitType.MINUTE_TENS, DigitType.MINUTE_UNITS -> {
- width = smallDigitWidth
- height = smallDigitHeight
- paint = digitTextMinutesPaint
- }
- DigitType.SECOND_TENS, DigitType.SECOND_UNITS -> {
- width = smallDigitWidth
- height = smallDigitHeight
- paint = digitTextSecondsPaint
- }
- }
+ // The width of the small digit bitmaps, as a fraction of their height.
+ private const val SMALL_DIGIT_WIDTH_FRACTION = 0.7f
- val bitmap =
- digitBitmapCache.get(digitType.ordinal) ?: createBitmap(width, height, digitType)
- if (digit != currentCachedDigits.get(digitType.ordinal)) {
- currentCachedDigits.put(digitType.ordinal, digit)
+ // The padding at the top and bottom of the digit bitmaps, given as a fraction of the height.
+ // Needed as some characters may ascend or descend slightly (e.g. "8").
+ private const val DIGIT_PADDING_FRACTION = 0.05f
- bitmap.eraseColor(Color.TRANSPARENT)
- val canvas = Canvas(bitmap)
- val textWidth = paint.measureText(digit)
+ // The gap between the hours and the minutes/seconds, given as a fraction of the width of the large
+ // digits.
+ private const val GAP_WIDTH_FRACTION = 0.1f
- // We use the same vertical padding here for all types and sizes of digit so that
- // their tops and bottoms can be aligned.
- canvas.drawText(
- digit,
- (width - textWidth) / 2,
- height.toFloat() - digitVerticalPadding,
- paint
+ // A string containing all digits, used to measure their height.
+ private const val ALL_DIGITS = "0123456789"
+
+ private val DIGITS = ALL_DIGITS.toCharArray().map { it.toString() }
+
+ // The start and end times of the animations, expressed as a fraction of a second.
+ // (So 0.5 means that the animation of that digit will begin half-way through the second).
+ // Note that because we only cache one digit of each type, the current and next times must
+ // not overlap.
+ private val DIGIT_ANIMATION_START_TIME_FRACTION = mapOf(
+ DigitMode.OUTGOING to 0.5f,
+ DigitMode.INCOMING to 0.667f
+ )
+ private val DIGIT_ANIMATION_END_TIME = mapOf(
+ DigitMode.OUTGOING to 0.667f,
+ DigitMode.INCOMING to 1f
+ )
+ private const val POSITION_ANIMATION_START_TIME = 0.0833f
+ private const val POSITION_ANIMATION_END_TIME = 0.5833f
+
+ // Parameters governing the animation of the current and next digits. NB Scale is a size multiplier.
+ // The first index is the values for the outgoing digit, and the second index for the incoming
+ // digit. If seconds are changing from 1 -> 2 for example, the 1 will scale from 1f to 0.65f, and
+ // rotate from 0f to 82f. The 2 will scale from 0.65f to 1f, and rotate from -97f to 0f.
+ private val DIGIT_SCALE_START = mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.65f)
+ private val DIGIT_SCALE_END = mapOf(DigitMode.OUTGOING to 0.65f, DigitMode.INCOMING to 1f)
+ private val DIGIT_ROTATE_START_DEGREES = mapOf(
+ DigitMode.OUTGOING to 0f,
+ DigitMode.INCOMING to -97f
+ )
+ private val DIGIT_ROTATE_END_DEGREES =
+ mapOf(DigitMode.OUTGOING to 82f, DigitMode.INCOMING to 0f)
+ private val DIGIT_OPACITY_START =
+ mapOf(DigitMode.OUTGOING to 1f, DigitMode.INCOMING to 0.07f)
+ private val DIGIT_OPACITY_END = mapOf(DigitMode.OUTGOING to 0f, DigitMode.INCOMING to 1f)
+
+ // The offset used to stagger the animation when multiple digits are animating at the same time.
+ private const val TIME_OFFSET_SECONDS_PER_DIGIT_TYPE = -5 / 60f
+
+ // The duration of the ambient mode change animation.
+ private const val AMBIENT_TRANSITION_MS = 333L
+
+ private val DIGIT_SCALE_INTERPOLATOR = mapOf(
+ DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 0.67f, 1f),
+ DigitMode.INCOMING to PathInterpolator(0.33f, 0f, 0.2f, 1f)
+ )
+ private val DIGIT_ROTATION_INTERPOLATOR = mapOf(
+ DigitMode.OUTGOING to PathInterpolator(0.57f, 0f, 0.73f, 0.49f),
+ DigitMode.INCOMING to PathInterpolator(0.15f, 0.49f, 0.37f, 1f)
+ )
+ private val DIGIT_OPACITY_INTERPOLATOR = mapOf(
+ DigitMode.OUTGOING to PathInterpolator(0.4f, 0f, 1f, 1f),
+ DigitMode.INCOMING to PathInterpolator(0f, 0f, 0.2f, 1f)
+ )
+ private val CENTERING_ADJUSTMENT_INTERPOLATOR =
+ PathInterpolator(0.4f, 0f, 0.2f, 1f)
+
+ @ColorInt
+ private fun colorRgb(red: Float, green: Float, blue: Float) =
+ 0xff000000.toInt() or
+ ((red * 255.0f + 0.5f).toInt() shl 16) or
+ ((green * 255.0f + 0.5f).toInt() shl 8) or
+ (blue * 255.0f + 0.5f).toInt()
+
+ private fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
+
+ private fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
+
+ private fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
+
+ /**
+ * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
+ * `backgroundColor` background.
+ *
+ * @param color the foreground color
+ * @param alphaFraction the fraction of the alpha value, range from 0 to 1
+ * @param backgroundColor the background color
+ */
+ private fun getRGBColor(
+ @ColorInt color: Int,
+ alphaFraction: Float,
+ @ColorInt backgroundColor: Int
+ ): Int {
+ return colorRgb(
+ lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
+ lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
+ lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
)
}
- return bitmap
+
+ /** Returns a linear interpolation between a and b using the scalar s. */
+ private fun lerp(a: Float, b: Float, s: Float) = a + s * (b - a)
+
+ /**
+ * Returns the interpolation scalar (s) that satisfies the equation: `value = lerp(a, b, s)`
+ *
+ * If `a == b`, then this function will return 0.
+ */
+ private fun lerpInv(a: Float, b: Float, value: Float) =
+ if (a != b) (value - a) / (b - a) else 0.0f
+
+ private fun getInterpolatedValue(
+ startValue: Float,
+ endValue: Float,
+ startTime: Float,
+ endTime: Float,
+ currentTime: Float,
+ interpolator: TimeInterpolator
+ ): Float {
+ val progress = when {
+ currentTime < startTime -> 0f
+ currentTime > endTime -> 1f
+ else -> interpolator.getInterpolation(lerpInv(startTime, endTime, currentTime))
+ }
+ return lerp(startValue, endValue, progress)
+ }
+
+ /**
+ * Sets the [DigitDrawProperties] that should be used for drawing, given the specified
+ * parameters.
+ *
+ * @param secondProgress the sub-second part of the current time, where 0 means the current second
+ * has just begun, and 1 means the current second has just ended
+ * @param offsetSeconds a value added to the start and end time of the animations
+ * @param digitMode whether the digit is OUTGOING or INCOMING
+ * @param output the [DigitDrawProperties] that will be set
+ */
+ private fun getDigitDrawProperties(
+ secondProgress: Float,
+ offsetSeconds: Float,
+ digitMode: DigitMode,
+ output: DigitDrawProperties
+ ) {
+ val startTime = DIGIT_ANIMATION_START_TIME_FRACTION[digitMode]!! + offsetSeconds
+ val endTime = DIGIT_ANIMATION_END_TIME[digitMode]!! + offsetSeconds
+ output.shouldDraw = if (digitMode == DigitMode.OUTGOING) {
+ secondProgress < endTime
+ } else {
+ secondProgress >= startTime
+ }
+ output.scale = getInterpolatedValue(
+ DIGIT_SCALE_START[digitMode]!!,
+ DIGIT_SCALE_END[digitMode]!!,
+ startTime,
+ endTime,
+ secondProgress,
+ DIGIT_SCALE_INTERPOLATOR[digitMode]!!
+ )
+ output.rotation = getInterpolatedValue(
+ DIGIT_ROTATE_START_DEGREES[digitMode]!!,
+ DIGIT_ROTATE_END_DEGREES[digitMode]!!,
+ startTime,
+ endTime,
+ secondProgress,
+ DIGIT_ROTATION_INTERPOLATOR[digitMode]!!
+ )
+ output.opacity = getInterpolatedValue(
+ DIGIT_OPACITY_START[digitMode]!!,
+ DIGIT_OPACITY_END[digitMode]!!,
+ startTime,
+ endTime,
+ secondProgress,
+ DIGIT_OPACITY_INTERPOLATOR[digitMode]!!
+ )
+ }
+
+ private fun getTimeOffsetSeconds(digitType: DigitType): Float {
+ return when (digitType) {
+ DigitType.HOUR_TENS -> 5f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+ DigitType.HOUR_UNITS -> 4f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+ DigitType.MINUTE_TENS -> 3f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+ DigitType.MINUTE_UNITS -> 2f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+ DigitType.SECOND_TENS -> 1f * TIME_OFFSET_SECONDS_PER_DIGIT_TYPE
+ DigitType.SECOND_UNITS -> 0f
+ }
+ }
+
+ /** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
+ private fun multiplyColor(colorInt: Int, multiplier: Float): Int {
+ val adjustedMultiplier = multiplier / 255.0f
+ return colorRgb(
+ Color.red(colorInt).toFloat() * adjustedMultiplier,
+ Color.green(colorInt).toFloat() * adjustedMultiplier,
+ Color.blue(colorInt).toFloat() * adjustedMultiplier,
+ )
+ }
+
+ private fun createBoundsRect(
+ centerFraction: PointF,
+ size: Vec2f
+ ): RectF {
+ val halfWidth = size.x / 2.0f
+ val halfHeight = size.y / 2.0f
+ return RectF(
+ (centerFraction.x - halfWidth),
+ (centerFraction.y - halfHeight),
+ (centerFraction.x + halfWidth),
+ (centerFraction.y + halfHeight)
+ )
+ }
}
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 1ecd8a4..45a6c9b3 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -45,21 +45,6 @@
import kotlin.math.cos
import kotlin.math.sin
-private val timeText = charArrayOf('1', '0', ':', '0', '9')
-private val secondsText = charArrayOf('3', '0')
-
-@Px
-private val SECONDS_TEXT_HEIGHT = 180f
-
-@Px
-private val TIME_TEXT_ACTIVE_HEIGHT = 64f
-
-@Px
-private val TIME_TEXT_AMBIENT_HEIGHT = 96f
-
-@Px
-private val TEXT_PADDING = 12
-
open class ExampleHierarchicalStyleWatchFaceService : WatchFaceService() {
internal val twelveHourClockOption by lazy {
@@ -271,159 +256,188 @@
}
}
)
-}
-class ExampleHierarchicalStyleWatchFaceRenderer {
- internal val paint = Paint().apply {
- textAlign = Paint.Align.CENTER
- isAntiAlias = true
- }
-
- internal val textBounds = Rect()
-
- @Px
- internal val timeActiveHeight = Rect().apply {
- paint.textSize = TIME_TEXT_ACTIVE_HEIGHT
- paint.getTextBounds(timeText, 0, timeText.size, this)
- }.height()
-
- @Px
- internal val timeAmbientHeight = Rect().apply {
- paint.textSize = TIME_TEXT_AMBIENT_HEIGHT
- paint.getTextBounds(timeText, 0, timeText.size, this)
- }.height()
-
- @Px
- internal val secondsHeight = Rect().apply {
- paint.textSize = SECONDS_TEXT_HEIGHT
- paint.getTextBounds(secondsText, 0, secondsText.size, this)
- }.height()
-
- @Px
- internal val timeActiveOffset =
- (timeActiveHeight + secondsHeight + TEXT_PADDING) / 2 - timeActiveHeight
-
- @Px
- internal val timeAmbientOffset = timeAmbientHeight / 2 - timeAmbientHeight
-
- @Px
- internal val secondsActiveOffset = timeActiveOffset - secondsHeight - TEXT_PADDING
-
- fun renderDigital(
- canvas: Canvas,
- bounds: Rect,
- zonedDateTime: ZonedDateTime,
- renderParameters: RenderParameters,
- watchState: WatchState,
- twentyFourHour: Boolean,
- color: IntArray
- ) {
- val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
- val hour = zonedDateTime.hour % if (twentyFourHour) 24 else 12
- val minute = zonedDateTime.minute
- val second = zonedDateTime.second
-
- paint.color = if (isActive) {
- val scale = 64 + 192 * second / 60
- Color.rgb(color[0] * scale, color[1] * scale, color[2] * scale)
- } else {
- Color.BLACK
+ private class ExampleHierarchicalStyleWatchFaceRenderer {
+ internal val paint = Paint().apply {
+ textAlign = Paint.Align.CENTER
+ isAntiAlias = true
}
- canvas.drawRect(bounds, paint)
- paint.color = Color.WHITE
- timeText[0] = ('0' + hour / 10)
- timeText[1] = ('0' + hour % 10)
- timeText[2] = if (second % 2 == 0) ':' else ' '
- timeText[3] = ('0' + minute / 10)
- timeText[4] = ('0' + minute % 10)
- paint.textSize = if (isActive) TIME_TEXT_ACTIVE_HEIGHT else TIME_TEXT_AMBIENT_HEIGHT
- val timeOffset = if (isActive) timeActiveOffset else timeAmbientOffset
- canvas.drawText(
- timeText,
- 0,
- timeText.size,
- bounds.centerX().toFloat(),
- (bounds.centerY() - watchState.chinHeight - timeOffset).toFloat(),
- paint
- )
+ internal val textBounds = Rect()
- paint.textSize = SECONDS_TEXT_HEIGHT
- if (isActive) {
- secondsText[0] = ('0' + second / 10)
- secondsText[1] = ('0' + second % 10)
+ @Px
+ internal val timeActiveHeight = Rect().apply {
+ paint.textSize = TIME_TEXT_ACTIVE_HEIGHT
+ paint.getTextBounds(timeText, 0, timeText.size, this)
+ }.height()
+
+ @Px
+ internal val timeAmbientHeight = Rect().apply {
+ paint.textSize = TIME_TEXT_AMBIENT_HEIGHT
+ paint.getTextBounds(timeText, 0, timeText.size, this)
+ }.height()
+
+ @Px
+ internal val secondsHeight = Rect().apply {
+ paint.textSize = SECONDS_TEXT_HEIGHT
+ paint.getTextBounds(secondsText, 0, secondsText.size, this)
+ }.height()
+
+ @Px
+ internal val timeActiveOffset =
+ (timeActiveHeight + secondsHeight + TEXT_PADDING) / 2 - timeActiveHeight
+
+ @Px
+ internal val timeAmbientOffset = timeAmbientHeight / 2 - timeAmbientHeight
+
+ @Px
+ internal val secondsActiveOffset = timeActiveOffset - secondsHeight - TEXT_PADDING
+
+ fun renderDigital(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime,
+ renderParameters: RenderParameters,
+ watchState: WatchState,
+ twentyFourHour: Boolean,
+ color: IntArray
+ ) {
+ val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
+ val hour = zonedDateTime.hour % if (twentyFourHour) 24 else 12
+ val minute = zonedDateTime.minute
+ val second = zonedDateTime.second
+
+ paint.color = if (isActive) {
+ val scale = 64 + 192 * second / 60
+ Color.rgb(color[0] * scale, color[1] * scale, color[2] * scale)
+ } else {
+ Color.BLACK
+ }
+ canvas.drawRect(bounds, paint)
+
+ paint.color = Color.WHITE
+ timeText[0] = ('0' + hour / 10)
+ timeText[1] = ('0' + hour % 10)
+ timeText[2] = if (second % 2 == 0) ':' else ' '
+ timeText[3] = ('0' + minute / 10)
+ timeText[4] = ('0' + minute % 10)
+ paint.textSize = if (isActive) TIME_TEXT_ACTIVE_HEIGHT else TIME_TEXT_AMBIENT_HEIGHT
+ val timeOffset = if (isActive) timeActiveOffset else timeAmbientOffset
canvas.drawText(
- secondsText,
+ timeText,
0,
- secondsText.size,
+ timeText.size,
bounds.centerX().toFloat(),
- (bounds.centerY() - watchState.chinHeight - secondsActiveOffset).toFloat(),
+ (bounds.centerY() - watchState.chinHeight - timeOffset).toFloat(),
paint
)
- }
- }
- fun renderAnalog(
- canvas: Canvas,
- bounds: Rect,
- zonedDateTime: ZonedDateTime,
- renderParameters: RenderParameters,
- hoursDrawFreq: Int,
- watchHandLength: Float
- ) {
- val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
-
- paint.color = Color.BLACK
- canvas.drawRect(bounds, paint)
-
- paint.color = Color.WHITE
- paint.textSize = 20.0f
- if (isActive) {
- for (i in 12 downTo 1 step hoursDrawFreq) {
- val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
- val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
- val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
- val mark = i.toString()
- paint.getTextBounds(mark, 0, mark.length, textBounds)
+ paint.textSize = SECONDS_TEXT_HEIGHT
+ if (isActive) {
+ secondsText[0] = ('0' + second / 10)
+ secondsText[1] = ('0' + second % 10)
canvas.drawText(
- mark,
- bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
- bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+ secondsText,
+ 0,
+ secondsText.size,
+ bounds.centerX().toFloat(),
+ (bounds.centerY() - watchState.chinHeight - secondsActiveOffset).toFloat(),
paint
)
}
}
- val hours = (zonedDateTime.hour % 12).toFloat()
- val minutes = zonedDateTime.minute.toFloat()
- val seconds = zonedDateTime.second.toFloat() +
- (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+ fun renderAnalog(
+ canvas: Canvas,
+ bounds: Rect,
+ zonedDateTime: ZonedDateTime,
+ renderParameters: RenderParameters,
+ hoursDrawFreq: Int,
+ watchHandLength: Float
+ ) {
+ val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
- val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
- val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+ paint.color = Color.BLACK
+ canvas.drawRect(bounds, paint)
- val hourXRadius = bounds.width() * watchHandLength * 0.35f
- val hourYRadius = bounds.height() * watchHandLength * 0.35f
+ paint.color = Color.WHITE
+ paint.textSize = 20.0f
+ if (isActive) {
+ for (i in 12 downTo 1 step hoursDrawFreq) {
+ val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
+ val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+ val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
+ val mark = i.toString()
+ paint.getTextBounds(mark, 0, mark.length, textBounds)
+ canvas.drawText(
+ mark,
+ bounds.exactCenterX() + dx - textBounds.width() / 2.0f,
+ bounds.exactCenterY() + dy + textBounds.height() / 2.0f,
+ paint
+ )
+ }
+ }
- paint.strokeWidth = if (isActive) 8f else 5f
- canvas.drawLine(
- bounds.exactCenterX(),
- bounds.exactCenterY(),
- bounds.exactCenterX() + sin(hourRot) * hourXRadius,
- bounds.exactCenterY() - cos(hourRot) * hourYRadius,
- paint
- )
+ val hours = (zonedDateTime.hour % 12).toFloat()
+ val minutes = zonedDateTime.minute.toFloat()
+ val seconds = zonedDateTime.second.toFloat() +
+ (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
- val minuteXRadius = bounds.width() * watchHandLength * 0.499f
- val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+ val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+ val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
- paint.strokeWidth = if (isActive) 4f else 2.5f
- canvas.drawLine(
- bounds.exactCenterX(),
- bounds.exactCenterY(),
- bounds.exactCenterX() + sin(minuteRot) * minuteXRadius,
- bounds.exactCenterY() - cos(minuteRot) * minuteYRadius,
- paint
- )
+ val hourXRadius = bounds.width() * watchHandLength * 0.35f
+ val hourYRadius = bounds.height() * watchHandLength * 0.35f
+
+ paint.strokeWidth = if (isActive) 8f else 5f
+ canvas.drawLine(
+ bounds.exactCenterX(),
+ bounds.exactCenterY(),
+ bounds.exactCenterX() + sin(hourRot) * hourXRadius,
+ bounds.exactCenterY() - cos(hourRot) * hourYRadius,
+ paint
+ )
+
+ val minuteXRadius = bounds.width() * watchHandLength * 0.499f
+ val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+
+ paint.strokeWidth = if (isActive) 4f else 2.5f
+ canvas.drawLine(
+ bounds.exactCenterX(),
+ bounds.exactCenterY(),
+ bounds.exactCenterX() + sin(minuteRot) * minuteXRadius,
+ bounds.exactCenterY() - cos(minuteRot) * minuteYRadius,
+ paint
+ )
+ }
+ }
+
+ companion object {
+ private const val COLOR_STYLE_SETTING = "color_style_setting"
+ private const val RED_STYLE = "red_style"
+ private const val GREEN_STYLE = "green_style"
+ private const val BLUE_STYLE = "blue_style"
+
+ private const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+ private const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
+ private const val HOURS_DRAW_FREQ_MIN = 1L
+ private const val HOURS_DRAW_FREQ_MAX = 4L
+ private const val HOURS_DRAW_FREQ_DEFAULT = 3L
+ private const val NUMBER_RADIUS_FRACTION = 0.45f
+
+ private val timeText = charArrayOf('1', '0', ':', '0', '9')
+ private val secondsText = charArrayOf('3', '0')
+
+ @Px
+ private val SECONDS_TEXT_HEIGHT = 180f
+
+ @Px
+ private val TIME_TEXT_ACTIVE_HEIGHT = 64f
+
+ @Px
+ private val TIME_TEXT_AMBIENT_HEIGHT = 96f
+
+ @Px
+ private val TEXT_PADDING = 12
}
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
index 3e014f0..7e40c2a 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLBackgroundInitWatchFaceService.kt
@@ -39,12 +39,6 @@
import androidx.wear.watchface.style.WatchFaceLayer
import java.time.ZonedDateTime
-/** Expected frame rate in interactive mode. */
-private const val FPS: Long = 60
-
-/** How long each frame is displayed at expected frame rate. */
-private const val FRAME_PERIOD_MS: Long = 1000 / FPS
-
/**
* Sample watch face using OpenGL with textures loaded on a background thread by [createWatchFace]
* which are used for rendering on the main thread.
@@ -101,242 +95,250 @@
Intent(this, ComplicationRationalActivity::class.java)
)
}
-}
-// SharedAssets shared between each instance of the watch face.
-internal class ExampleSharedAssets(resources: Resources) : Renderer.SharedAssets {
- val yellowWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background)
- val blueWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background_blue)
- val watchHandTexture = loadTextureFromResource(resources, R.drawable.hand)
+ // SharedAssets shared between each instance of the watch face.
+ private class ExampleSharedAssets(resources: Resources) : Renderer.SharedAssets {
+ val yellowWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background)
+ val blueWatchBodyTexture = loadTextureFromResource(resources, R.drawable.wf_background_blue)
+ val watchHandTexture = loadTextureFromResource(resources, R.drawable.hand)
- val triangleTextureProgram = Gles2TexturedTriangleList.Program()
- val backgroundQuad = createTexturedQuad(
- triangleTextureProgram, -10f, -10f, 20f, 20f
- )
- val secondHandQuad = createTexturedQuad(
- triangleTextureProgram, -0.75f, -6f, 1.5f, 8f
- )
- val minuteHandQuad = createTexturedQuad(
- triangleTextureProgram, -0.33f, -4.5f, 0.66f, 6f
- )
- val hourHandQuad = createTexturedQuad(
- triangleTextureProgram, -0.25f, -3f, 0.5f, 4f
- )
-
- override fun onDestroy() {
- GLES20.glDeleteTextures(
- 3,
- intArrayOf(yellowWatchBodyTexture, blueWatchBodyTexture, watchHandTexture),
- 0
+ val triangleTextureProgram = Gles2TexturedTriangleList.Program()
+ val backgroundQuad = createTexturedQuad(
+ triangleTextureProgram, -10f, -10f, 20f, 20f
)
- checkGLError("glDeleteTextures")
+ val secondHandQuad = createTexturedQuad(
+ triangleTextureProgram, -0.75f, -6f, 1.5f, 8f
+ )
+ val minuteHandQuad = createTexturedQuad(
+ triangleTextureProgram, -0.33f, -4.5f, 0.66f, 6f
+ )
+ val hourHandQuad = createTexturedQuad(
+ triangleTextureProgram, -0.25f, -3f, 0.5f, 4f
+ )
- triangleTextureProgram.onDestroy()
+ override fun onDestroy() {
+ GLES20.glDeleteTextures(
+ 3,
+ intArrayOf(yellowWatchBodyTexture, blueWatchBodyTexture, watchHandTexture),
+ 0
+ )
+ checkGLError("glDeleteTextures")
+
+ triangleTextureProgram.onDestroy()
+ }
+
+ private fun createTexturedQuad(
+ program: Gles2TexturedTriangleList.Program,
+ left: Float,
+ top: Float,
+ width: Float,
+ height: Float
+ ) = Gles2TexturedTriangleList(
+ program,
+ floatArrayOf(
+ top + 0f,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + height,
+ left + width,
+ 0.0f
+ ),
+ floatArrayOf(
+ 1.0f,
+ 1.0f,
+
+ 1.0f,
+ 0.0f,
+
+ 0.0f,
+ 1.0f,
+
+ 1.0f,
+ 0.0f,
+
+ 0.0f,
+ 1.0f,
+
+ 0.0f,
+ 0.0f
+ )
+ )
+
+ private fun loadTextureFromResource(resources: Resources, resourceId: Int): Int {
+ val textureHandle = IntArray(1)
+ GLES20.glGenTextures(1, textureHandle, 0)
+ if (textureHandle[0] != 0) {
+ val bitmap = BitmapFactory.decodeResource(
+ resources,
+ resourceId,
+ BitmapFactory.Options().apply {
+ inScaled = false // No pre-scaling
+ }
+ )
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
+ checkGLError("glBindTexture")
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_MIN_FILTER,
+ GLES20.GL_NEAREST
+ )
+ GLES20.glTexParameteri(
+ GLES20.GL_TEXTURE_2D,
+ GLES20.GL_TEXTURE_MAG_FILTER,
+ GLES20.GL_NEAREST
+ )
+ GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
+ bitmap.recycle()
+ }
+ return textureHandle[0]
+ }
}
- private fun createTexturedQuad(
- program: Gles2TexturedTriangleList.Program,
- left: Float,
- top: Float,
- width: Float,
- height: Float
- ) = Gles2TexturedTriangleList(
- program,
- floatArrayOf(
- top + 0f,
- left + 0.0f,
- 0.0f,
+ private class MainThreadRenderer(
+ surfaceHolder: SurfaceHolder,
+ private val currentUserStyleRepository: CurrentUserStyleRepository,
+ watchState: WatchState,
+ private val resources: Resources,
+ private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
+ ) : Renderer.GlesRenderer2<ExampleSharedAssets>(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ FRAME_PERIOD_MS
+ ) {
+ private val projectionMatrix = FloatArray(16)
+ private val viewMatrix = FloatArray(16).apply {
+ Matrix.setLookAtM(
+ this,
+ 0,
+ 0f,
+ 0f,
+ -1.0f,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f,
+ 0f
+ )
+ }
+ private val handPositionMatrix = FloatArray(16)
+ private val handViewMatrix = FloatArray(16)
+ private val vpMatrix = FloatArray(16)
- top + 0.0f,
- left + width,
- 0.0f,
+ override suspend fun createSharedAssets() = ExampleSharedAssets(resources)
- top + height,
- left + 0.0f,
- 0.0f,
+ override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
+ GLES20.glEnable(GLES20.GL_TEXTURE_2D)
- top + 0.0f,
- left + width,
- 0.0f,
+ // Update the projection matrix based on the new aspect ratio.
+ val aspectRatio = width.toFloat() / height.toFloat()
+ Matrix.frustumM(
+ projectionMatrix,
+ 0 /* offset */,
+ -aspectRatio /* left */,
+ aspectRatio /* right */,
+ -1f /* bottom */,
+ 1f /* top */,
+ 0.1f /* near */,
+ 100f /* far */
+ )
+ }
- top + height,
- left + 0.0f,
- 0.0f,
+ override fun render(zonedDateTime: ZonedDateTime, sharedAssets: ExampleSharedAssets) {
+ checkGLError("renders")
+ GLES20.glClearColor(0f, 1f, 0f, 1f)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
- top + height,
- left + width,
- 0.0f
- ),
- floatArrayOf(
- 1.0f,
- 1.0f,
-
- 1.0f,
- 0.0f,
-
- 0.0f,
- 1.0f,
-
- 1.0f,
- 0.0f,
-
- 0.0f,
- 1.0f,
-
- 0.0f,
- 0.0f
- )
- )
-
- private fun loadTextureFromResource(resources: Resources, resourceId: Int): Int {
- val textureHandle = IntArray(1)
- GLES20.glGenTextures(1, textureHandle, 0)
- if (textureHandle[0] != 0) {
- val bitmap = BitmapFactory.decodeResource(
- resources,
- resourceId,
- BitmapFactory.Options().apply {
- inScaled = false // No pre-scaling
+ sharedAssets.triangleTextureProgram.bindProgramAndAttribs()
+ GLES20.glBindTexture(
+ GLES20.GL_TEXTURE_2D,
+ when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
+ "yellow_style" -> sharedAssets.yellowWatchBodyTexture
+ "blue_style" -> sharedAssets.blueWatchBodyTexture
+ else -> throw UnsupportedOperationException()
}
)
- GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureHandle[0])
- checkGLError("glBindTexture")
- GLES20.glTexParameteri(
- GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_MIN_FILTER,
- GLES20.GL_NEAREST
- )
- GLES20.glTexParameteri(
- GLES20.GL_TEXTURE_2D,
- GLES20.GL_TEXTURE_MAG_FILTER,
- GLES20.GL_NEAREST
- )
- GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0)
- bitmap.recycle()
+
+ GLES20.glEnable(GLES20.GL_BLEND)
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+
+ Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
+ sharedAssets.backgroundQuad.draw(vpMatrix)
+
+ GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
+ GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, sharedAssets.watchHandTexture)
+
+ val hours = (zonedDateTime.hour % 12).toFloat()
+ val minutes = zonedDateTime.minute.toFloat()
+ val seconds = zonedDateTime.second.toFloat() +
+ (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
+
+ val secondsRot = seconds / 60.0f * 360.0f
+ Matrix.setRotateM(handPositionMatrix, 0, secondsRot, 0f, 0f, 1f)
+ Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+ Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+ sharedAssets.secondHandQuad.draw(vpMatrix)
+
+ val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
+ Matrix.setRotateM(handPositionMatrix, 0, minuteRot, 0f, 0f, 1f)
+ Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+ Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+ sharedAssets.minuteHandQuad.draw(vpMatrix)
+
+ val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
+ Matrix.setRotateM(handPositionMatrix, 0, hourRot, 0f, 0f, 1f)
+ Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
+ Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
+ sharedAssets.hourHandQuad.draw(vpMatrix)
+
+ sharedAssets.triangleTextureProgram.unbindAttribs()
}
- return textureHandle[0]
- }
-}
-internal fun checkGLError(op: String) {
- var error: Int
- while (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {
- System.err.println("OpenGL Error $op: glError $error")
- }
-}
-
-internal class MainThreadRenderer(
- surfaceHolder: SurfaceHolder,
- private val currentUserStyleRepository: CurrentUserStyleRepository,
- watchState: WatchState,
- private val resources: Resources,
- private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
-) : Renderer.GlesRenderer2<ExampleSharedAssets>(
- surfaceHolder,
- currentUserStyleRepository,
- watchState,
- FRAME_PERIOD_MS
-) {
- private val projectionMatrix = FloatArray(16)
- private val viewMatrix = FloatArray(16).apply {
- Matrix.setLookAtM(
- this,
- 0,
- 0f,
- 0f,
- -1.0f,
- 0f,
- 0f,
- 0f,
- 0f,
- 1f,
- 0f
- )
- }
- private val handPositionMatrix = FloatArray(16)
- private val handViewMatrix = FloatArray(16)
- private val vpMatrix = FloatArray(16)
-
- override suspend fun createSharedAssets() = ExampleSharedAssets(resources)
-
- override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
- GLES20.glEnable(GLES20.GL_TEXTURE_2D)
-
- // Update the projection matrix based on the new aspect ratio.
- val aspectRatio = width.toFloat() / height.toFloat()
- Matrix.frustumM(
- projectionMatrix,
- 0 /* offset */,
- -aspectRatio /* left */,
- aspectRatio /* right */,
- -1f /* bottom */,
- 1f /* top */,
- 0.1f /* near */,
- 100f /* far */
- )
+ override fun renderHighlightLayer(
+ zonedDateTime: ZonedDateTime,
+ sharedAssets: ExampleSharedAssets
+ ) {
+ val highlightLayer = renderParameters.highlightLayer!!
+ GLES20.glClearColor(
+ Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
+ )
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
}
- override fun render(zonedDateTime: ZonedDateTime, sharedAssets: ExampleSharedAssets) {
- checkGLError("renders")
- GLES20.glClearColor(0f, 1f, 0f, 1f)
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ companion object {
+ /** Expected frame rate in interactive mode. */
+ private const val FPS: Long = 60
- sharedAssets.triangleTextureProgram.bindProgramAndAttribs()
- GLES20.glBindTexture(
- GLES20.GL_TEXTURE_2D,
- when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
- "yellow_style" -> sharedAssets.yellowWatchBodyTexture
- "blue_style" -> sharedAssets.blueWatchBodyTexture
- else -> throw UnsupportedOperationException()
+ /** How long each frame is displayed at expected frame rate. */
+ private const val FRAME_PERIOD_MS: Long = 1000 / FPS
+
+ private fun checkGLError(op: String) {
+ var error: Int
+ while (GLES20.glGetError().also { error = it } != GLES20.GL_NO_ERROR) {
+ System.err.println("OpenGL Error $op: glError $error")
}
- )
-
- GLES20.glEnable(GLES20.GL_BLEND)
- GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
-
- Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, viewMatrix, 0)
- sharedAssets.backgroundQuad.draw(vpMatrix)
-
- GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA)
- GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, sharedAssets.watchHandTexture)
-
- val hours = (zonedDateTime.hour % 12).toFloat()
- val minutes = zonedDateTime.minute.toFloat()
- val seconds = zonedDateTime.second.toFloat() +
- (zonedDateTime.nano.toDouble() / 1000000000.0).toFloat()
-
- val secondsRot = seconds / 60.0f * 360.0f
- Matrix.setRotateM(handPositionMatrix, 0, secondsRot, 0f, 0f, 1f)
- Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
- Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
- sharedAssets.secondHandQuad.draw(vpMatrix)
-
- val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
- Matrix.setRotateM(handPositionMatrix, 0, minuteRot, 0f, 0f, 1f)
- Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
- Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
- sharedAssets.minuteHandQuad.draw(vpMatrix)
-
- val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
- Matrix.setRotateM(handPositionMatrix, 0, hourRot, 0f, 0f, 1f)
- Matrix.multiplyMM(handViewMatrix, 0, viewMatrix, 0, handPositionMatrix, 0)
- Matrix.multiplyMM(vpMatrix, 0, projectionMatrix, 0, handViewMatrix, 0)
- sharedAssets.hourHandQuad.draw(vpMatrix)
-
- sharedAssets.triangleTextureProgram.unbindAttribs()
- }
-
- override fun renderHighlightLayer(
- zonedDateTime: ZonedDateTime,
- sharedAssets: ExampleSharedAssets
- ) {
- val highlightLayer = renderParameters.highlightLayer!!
- GLES20.glClearColor(
- Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
- )
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ }
}
}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index d205f90..72ec126 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -21,17 +21,10 @@
import android.graphics.RectF
import android.graphics.drawable.Icon
import android.opengl.GLES20
-import android.opengl.GLU
-import android.opengl.GLUtils
import android.opengl.Matrix
-import android.util.Log
import android.view.Gravity
import android.view.SurfaceHolder
import androidx.annotation.RequiresApi
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.ComplicationSlot
import androidx.wear.watchface.ComplicationSlotsManager
import androidx.wear.watchface.DrawMode
@@ -42,6 +35,10 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationDeniedActivity
import androidx.wear.watchface.complications.permission.dialogs.sample.ComplicationRationalActivity
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
@@ -52,9 +49,6 @@
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import java.nio.FloatBuffer
import java.time.ZonedDateTime
import kotlin.math.cos
import kotlin.math.sin
@@ -62,23 +56,6 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
-/** Expected frame rate in interactive mode. */
-private const val FPS: Long = 60
-
-/** Z distance from the camera to the watchface. */
-private const val EYE_Z = -2.3f
-
-/** How long each frame is displayed at expected frame rate. */
-private const val FRAME_PERIOD_MS: Long = 1000 / FPS
-
-/** Cycle time before the camera motion repeats. */
-private const val CYCLE_PERIOD_SECONDS: Long = 5
-
-/** Number of camera angles to precompute. */
-private const val numCameraAngles = (CYCLE_PERIOD_SECONDS * FPS).toInt()
-
-const val EXAMPLE_OPENGL_COMPLICATION_ID = 101
-
/**
* Sample watch face using OpenGL. The watch face is rendered using
* [Gles2ColoredTriangleList]s. The camera moves around in interactive mode and stops moving
@@ -128,6 +105,8 @@
},
listOf(
ComplicationType.RANGED_VALUE,
+ ComplicationType.GOAL_PROGRESS,
+ ComplicationType.WEIGHTED_ELEMENTS,
ComplicationType.LONG_TEXT,
ComplicationType.SHORT_TEXT,
ComplicationType.MONOCHROMATIC_IMAGE,
@@ -174,191 +153,209 @@
.setComplicationRationaleDialogIntent(
Intent(this, ComplicationRationalActivity::class.java)
)
-}
-@OptIn(WatchFaceExperimental::class)
-@Suppress("Deprecation")
-@RequiresApi(27)
-class ExampleOpenGLRenderer(
- surfaceHolder: SurfaceHolder,
- private val currentUserStyleRepository: CurrentUserStyleRepository,
- watchState: WatchState,
- private val colorStyleSetting: ListUserStyleSetting,
- private val complicationSlot: ComplicationSlot
-) : Renderer.GlesRenderer(surfaceHolder, currentUserStyleRepository, watchState, FRAME_PERIOD_MS) {
+ @OptIn(WatchFaceExperimental::class)
+ @Suppress("Deprecation")
+ @RequiresApi(27)
+ private class ExampleOpenGLRenderer(
+ surfaceHolder: SurfaceHolder,
+ private val currentUserStyleRepository: CurrentUserStyleRepository,
+ watchState: WatchState,
+ private val colorStyleSetting: ListUserStyleSetting,
+ private val complicationSlot: ComplicationSlot
+ ) : Renderer.GlesRenderer(
+ surfaceHolder,
+ currentUserStyleRepository,
+ watchState,
+ FRAME_PERIOD_MS
+ ) {
- /** Projection transformation matrix. Converts from 3D to 2D. */
- private val projectionMatrix = FloatArray(16)
+ /** Projection transformation matrix. Converts from 3D to 2D. */
+ private val projectionMatrix = FloatArray(16)
- /**
- * View transformation matrices to use in interactive mode. Converts from world to camera-
- * relative coordinates. One matrix per camera position.
- */
- private val viewMatrices = Array(numCameraAngles) { FloatArray(16) }
+ /**
+ * View transformation matrices to use in interactive mode. Converts from world to camera-
+ * relative coordinates. One matrix per camera position.
+ */
+ private val viewMatrices = Array(numCameraAngles) { FloatArray(16) }
- /** The view transformation matrix to use in ambient mode */
- private val ambientViewMatrix = FloatArray(16)
+ /** The view transformation matrix to use in ambient mode */
+ private val ambientViewMatrix = FloatArray(16)
- /**
- * Model transformation matrices. Converts from model-relative coordinates to world
- * coordinates. One matrix per degree of rotation.
- */
- private val modelMatrices = Array(360) { FloatArray(16) }
+ /**
+ * Model transformation matrices. Converts from model-relative coordinates to world
+ * coordinates. One matrix per degree of rotation.
+ */
+ private val modelMatrices = Array(360) { FloatArray(16) }
- /**
- * Products of [viewMatrices] and [projectionMatrix]. One matrix per camera
- * position.
- */
- private val vpMatrices = Array(numCameraAngles) { FloatArray(16) }
+ /**
+ * Products of [viewMatrices] and [projectionMatrix]. One matrix per camera
+ * position.
+ */
+ private val vpMatrices = Array(numCameraAngles) { FloatArray(16) }
- /** The product of [ambientViewMatrix] and [projectionMatrix] */
- private val ambientVpMatrix = FloatArray(16)
+ /** The product of [ambientViewMatrix] and [projectionMatrix] */
+ private val ambientVpMatrix = FloatArray(16)
- /**
- * Product of [modelMatrices], [viewMatrices], and
- * [projectionMatrix].
- */
- private val mvpMatrix = FloatArray(16)
+ /**
+ * Product of [modelMatrices], [viewMatrices], and
+ * [projectionMatrix].
+ */
+ private val mvpMatrix = FloatArray(16)
- /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */
- private lateinit var majorTickTriangles: Gles2ColoredTriangleList
+ /** Triangles for the 4 major ticks. These are grouped together to speed up rendering. */
+ private lateinit var majorTickTriangles: Gles2ColoredTriangleList
- /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */
- private lateinit var minorTickTriangles: Gles2ColoredTriangleList
+ /** Triangles for the 8 minor ticks. These are grouped together to speed up rendering. */
+ private lateinit var minorTickTriangles: Gles2ColoredTriangleList
- /** Triangle for the second hand. */
- private lateinit var secondHandTriangleMap: Map<String, Gles2ColoredTriangleList>
+ /** Triangle for the second hand. */
+ private lateinit var secondHandTriangleMap: Map<String, Gles2ColoredTriangleList>
- /** Triangle for the minute hand. */
- private lateinit var minuteHandTriangle: Gles2ColoredTriangleList
+ /** Triangle for the minute hand. */
+ private lateinit var minuteHandTriangle: Gles2ColoredTriangleList
- /** Triangle for the hour hand. */
- private lateinit var hourHandTriangle: Gles2ColoredTriangleList
+ /** Triangle for the hour hand. */
+ private lateinit var hourHandTriangle: Gles2ColoredTriangleList
- private lateinit var complicationTexture: GlesTextureComplication
+ private lateinit var complicationTexture: GlesTextureComplication
- private lateinit var coloredTriangleProgram: Gles2ColoredTriangleList.Program
- private lateinit var textureTriangleProgram: Gles2TexturedTriangleList.Program
+ private lateinit var coloredTriangleProgram: Gles2ColoredTriangleList.Program
+ private lateinit var textureTriangleProgram: Gles2TexturedTriangleList.Program
- private lateinit var complicationTriangles: Gles2TexturedTriangleList
- private lateinit var complicationHighlightTriangles: Gles2ColoredTriangleList
+ private lateinit var complicationTriangles: Gles2TexturedTriangleList
+ private lateinit var complicationHighlightTriangles: Gles2ColoredTriangleList
- init {
- CoroutineScope(Dispatchers.Main.immediate).launch {
- currentUserStyleRepository.userStyle.collect { userStyle ->
- watchfaceColors = when (userStyle[colorStyleSetting]!!.toString()) {
- "red_style" -> WatchFaceColors(
- Color.valueOf(0.5f, 0.2f, 0.2f, 1f),
- Color.valueOf(0.4f, 0.15f, 0.15f, 1f),
- Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
- )
+ init {
+ CoroutineScope(Dispatchers.Main.immediate).launch {
+ currentUserStyleRepository.userStyle.collect { userStyle ->
+ watchfaceColors = when (userStyle[colorStyleSetting]!!.toString()) {
+ "red_style" -> WatchFaceColors(
+ Color.valueOf(0.5f, 0.2f, 0.2f, 1f),
+ Color.valueOf(0.4f, 0.15f, 0.15f, 1f),
+ Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
+ )
- "green_style" -> WatchFaceColors(
- Color.valueOf(0.2f, 0.5f, 0.2f, 1f),
- Color.valueOf(0.15f, 0.4f, 0.15f, 1f),
- Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
- )
+ "green_style" -> WatchFaceColors(
+ Color.valueOf(0.2f, 0.5f, 0.2f, 1f),
+ Color.valueOf(0.15f, 0.4f, 0.15f, 1f),
+ Color.valueOf(0.1f, 0.1f, 0.1f, 1f)
+ )
- else -> null
+ else -> null
+ }
}
}
}
- }
- override suspend fun onBackgroundThreadGlContextCreated() {
- // Create program for drawing triangles.
- coloredTriangleProgram = Gles2ColoredTriangleList.Program()
+ override suspend fun onBackgroundThreadGlContextCreated() {
+ // Create program for drawing triangles.
+ coloredTriangleProgram = Gles2ColoredTriangleList.Program()
- // Create triangles for the ticks.
- majorTickTriangles = createMajorTicks(coloredTriangleProgram)
- minorTickTriangles = createMinorTicks(coloredTriangleProgram)
+ // Create triangles for the ticks.
+ majorTickTriangles = createMajorTicks(coloredTriangleProgram)
+ minorTickTriangles = createMinorTicks(coloredTriangleProgram)
- // Create program for drawing triangles.
- textureTriangleProgram = Gles2TexturedTriangleList.Program()
- complicationTriangles = createComplicationQuad(
- textureTriangleProgram,
- -0.9f,
- -0.1f,
- 0.6f,
- 0.6f
- )
+ // Create program for drawing triangles.
+ textureTriangleProgram = Gles2TexturedTriangleList.Program()
+ complicationTriangles = createComplicationQuad(
+ textureTriangleProgram,
+ -0.9f,
+ -0.1f,
+ 0.6f,
+ 0.6f
+ )
- complicationHighlightTriangles = createComplicationHighlightQuad(
- coloredTriangleProgram,
- -0.9f,
- -0.1f,
- 0.6f,
- 0.6f
- )
+ complicationHighlightTriangles = createComplicationHighlightQuad(
+ coloredTriangleProgram,
+ -0.9f,
+ -0.1f,
+ 0.6f,
+ 0.6f
+ )
- // Create triangles for the hands.
- secondHandTriangleMap = mapOf(
- "red_style" to
- createHand(
- coloredTriangleProgram,
- 0.02f /* width */,
- 1.0f /* height */,
- floatArrayOf(
- 1.0f /* red */,
- 0.0f /* green */,
- 0.0f /* blue */,
- 1.0f /* alpha */
+ // Create triangles for the hands.
+ secondHandTriangleMap = mapOf(
+ "red_style" to
+ createHand(
+ coloredTriangleProgram,
+ 0.02f /* width */,
+ 1.0f /* height */,
+ floatArrayOf(
+ 1.0f /* red */,
+ 0.0f /* green */,
+ 0.0f /* blue */,
+ 1.0f /* alpha */
+ )
+ ),
+ "greenstyle" to
+ createHand(
+ coloredTriangleProgram,
+ 0.02f /* width */,
+ 1.0f /* height */,
+ floatArrayOf(
+ 0.0f /* red */,
+ 1.0f /* green */,
+ 0.0f /* blue */,
+ 1.0f /* alpha */
+ )
)
- ),
- "greenstyle" to
- createHand(
- coloredTriangleProgram,
- 0.02f /* width */,
- 1.0f /* height */,
- floatArrayOf(
- 0.0f /* red */,
- 1.0f /* green */,
- 0.0f /* blue */,
- 1.0f /* alpha */
- )
+ )
+ minuteHandTriangle = createHand(
+ coloredTriangleProgram,
+ 0.06f /* width */,
+ 1f /* height */,
+ floatArrayOf(
+ 0.7f /* red */,
+ 0.7f /* green */,
+ 0.7f /* blue */,
+ 1.0f /* alpha */
)
- )
- minuteHandTriangle = createHand(
- coloredTriangleProgram,
- 0.06f /* width */,
- 1f /* height */,
- floatArrayOf(
- 0.7f /* red */,
- 0.7f /* green */,
- 0.7f /* blue */,
- 1.0f /* alpha */
)
- )
- hourHandTriangle = createHand(
- coloredTriangleProgram,
- 0.1f /* width */,
- 0.6f /* height */,
- floatArrayOf(
- 0.9f /* red */,
- 0.9f /* green */,
- 0.9f /* blue */,
- 1.0f /* alpha */
+ hourHandTriangle = createHand(
+ coloredTriangleProgram,
+ 0.1f /* width */,
+ 0.6f /* height */,
+ floatArrayOf(
+ 0.9f /* red */,
+ 0.9f /* green */,
+ 0.9f /* blue */,
+ 1.0f /* alpha */
+ )
)
- )
- // Precompute the clock angles.
- for (i in modelMatrices.indices) {
- Matrix.setRotateM(modelMatrices[i], 0, i.toFloat(), 0f, 0f, 1f)
- }
+ // Precompute the clock angles.
+ for (i in modelMatrices.indices) {
+ Matrix.setRotateM(modelMatrices[i], 0, i.toFloat(), 0f, 0f, 1f)
+ }
- // Precompute the camera angles.
- for (i in 0 until numCameraAngles) {
- // Set the camera position (View matrix). When active, move the eye around to show
- // off that this is 3D.
- val cameraAngle =
- (i.toFloat() / numCameraAngles * 2 * Math.PI).toFloat()
- val eyeX = Math.cos(cameraAngle.toDouble()).toFloat()
- val eyeY = Math.sin(cameraAngle.toDouble()).toFloat()
+ // Precompute the camera angles.
+ for (i in 0 until numCameraAngles) {
+ // Set the camera position (View matrix). When active, move the eye around to show
+ // off that this is 3D.
+ val cameraAngle =
+ (i.toFloat() / numCameraAngles * 2 * Math.PI).toFloat()
+ val eyeX = Math.cos(cameraAngle.toDouble()).toFloat()
+ val eyeY = Math.sin(cameraAngle.toDouble()).toFloat()
+ Matrix.setLookAtM(
+ viewMatrices[i],
+ 0,
+ eyeX,
+ eyeY,
+ EYE_Z,
+ 0f,
+ 0f,
+ 0f,
+ 0f,
+ 1f,
+ 0f
+ ) // up vector
+ }
Matrix.setLookAtM(
- viewMatrices[i],
+ ambientViewMatrix,
0,
- eyeX,
- eyeY,
+ 0f,
+ 0f,
EYE_Z,
0f,
0f,
@@ -367,1025 +364,422 @@
1f,
0f
) // up vector
- }
- Matrix.setLookAtM(
- ambientViewMatrix,
- 0,
- 0f,
- 0f,
- EYE_Z,
- 0f,
- 0f,
- 0f,
- 0f,
- 1f,
- 0f
- ) // up vector
- complicationTexture = GlesTextureComplication(
- complicationSlot,
- 128,
- 128,
- GLES20.GL_TEXTURE_2D
- )
- }
-
- override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
- // Update the projection matrix based on the new aspect ratio.
- val aspectRatio = width.toFloat() / height
- Matrix.frustumM(
- projectionMatrix,
- 0 /* offset */,
- -aspectRatio /* left */,
- aspectRatio /* right */,
- -1f /* bottom */,
- 1f /* top */,
- 2f /* near */,
- 7f /* far */
- )
-
- // Precompute the products of Projection and View matrices for each camera angle.
- for (i in 0 until numCameraAngles) {
- Matrix.multiplyMM(vpMatrices[i], 0, projectionMatrix, 0, viewMatrices[i], 0)
- }
- Matrix.multiplyMM(ambientVpMatrix, 0, projectionMatrix, 0, ambientViewMatrix, 0)
- }
-
- /**
- * Creates a triangle for a hand on the watch face.
- *
- * @param program program for drawing triangles
- * @param width width of base of triangle
- * @param length length of triangle
- * @param color color in RGBA order, each in the range [0, 1]
- */
- private fun createHand(
- program: Gles2ColoredTriangleList.Program,
- width: Float,
- length: Float,
- color: FloatArray
- ) = Gles2ColoredTriangleList(
- program,
- floatArrayOf(
- 0f,
- length,
- 0f,
- -width / 2,
- 0f,
- 0f,
- width / 2,
- 0f,
- 0f
- ),
- color
- )
-
- /**
- * Creates a triangle list for the major ticks on the watch face.
- *
- * @param program program for drawing triangles
- */
- private fun createMajorTicks(
- program: Gles2ColoredTriangleList.Program
- ): Gles2ColoredTriangleList {
- // Create the data for the VBO.
- val trianglesCoords = FloatArray(9 * 4)
- for (i in 0..3) {
- val triangleCoords = getMajorTickTriangleCoords(i)
- System.arraycopy(
- triangleCoords,
- 0,
- trianglesCoords,
- i * 9,
- triangleCoords.size
+ complicationTexture = GlesTextureComplication(
+ complicationSlot,
+ 128,
+ 128,
+ GLES20.GL_TEXTURE_2D
)
}
- return Gles2ColoredTriangleList(
- program, trianglesCoords,
+
+ override suspend fun onUiThreadGlSurfaceCreated(width: Int, height: Int) {
+ // Update the projection matrix based on the new aspect ratio.
+ val aspectRatio = width.toFloat() / height
+ Matrix.frustumM(
+ projectionMatrix,
+ 0 /* offset */,
+ -aspectRatio /* left */,
+ aspectRatio /* right */,
+ -1f /* bottom */,
+ 1f /* top */,
+ 2f /* near */,
+ 7f /* far */
+ )
+
+ // Precompute the products of Projection and View matrices for each camera angle.
+ for (i in 0 until numCameraAngles) {
+ Matrix.multiplyMM(vpMatrices[i], 0, projectionMatrix, 0, viewMatrices[i], 0)
+ }
+ Matrix.multiplyMM(ambientVpMatrix, 0, projectionMatrix, 0, ambientViewMatrix, 0)
+ }
+
+ /**
+ * Creates a triangle for a hand on the watch face.
+ *
+ * @param program program for drawing triangles
+ * @param width width of base of triangle
+ * @param length length of triangle
+ * @param color color in RGBA order, each in the range [0, 1]
+ */
+ private fun createHand(
+ program: Gles2ColoredTriangleList.Program,
+ width: Float,
+ length: Float,
+ color: FloatArray
+ ) = Gles2ColoredTriangleList(
+ program,
+ floatArrayOf(
+ 0f,
+ length,
+ 0f,
+ -width / 2,
+ 0f,
+ 0f,
+ width / 2,
+ 0f,
+ 0f
+ ),
+ color
+ )
+
+ /**
+ * Creates a triangle list for the major ticks on the watch face.
+ *
+ * @param program program for drawing triangles
+ */
+ private fun createMajorTicks(
+ program: Gles2ColoredTriangleList.Program
+ ): Gles2ColoredTriangleList {
+ // Create the data for the VBO.
+ val trianglesCoords = FloatArray(9 * 4)
+ for (i in 0..3) {
+ val triangleCoords = getMajorTickTriangleCoords(i)
+ System.arraycopy(
+ triangleCoords,
+ 0,
+ trianglesCoords,
+ i * 9,
+ triangleCoords.size
+ )
+ }
+ return Gles2ColoredTriangleList(
+ program, trianglesCoords,
+ floatArrayOf(
+ 1.0f /* red */,
+ 1.0f /* green */,
+ 1.0f /* blue */,
+ 1.0f /* alpha */
+ )
+ )
+ }
+
+ /**
+ * Creates a triangle list for the minor ticks on the watch face.
+ *
+ * @param program program for drawing triangles
+ */
+ private fun createMinorTicks(
+ program: Gles2ColoredTriangleList.Program
+ ): Gles2ColoredTriangleList {
+ // Create the data for the VBO.
+ val trianglesCoords = FloatArray(9 * (12 - 4))
+ var index = 0
+ for (i in 0..11) {
+ if (i % 3 == 0) {
+ // This is where a major tick goes, so skip it.
+ continue
+ }
+ val triangleCoords = getMinorTickTriangleCoords(i)
+ System.arraycopy(
+ triangleCoords,
+ 0,
+ trianglesCoords,
+ index,
+ triangleCoords.size
+ )
+ index += 9
+ }
+ return Gles2ColoredTriangleList(
+ program, trianglesCoords,
+ floatArrayOf(
+ 0.5f /* red */,
+ 0.5f /* green */,
+ 0.5f /* blue */,
+ 1.0f /* alpha */
+ )
+ )
+ }
+
+ /**
+ * Creates a triangle list for the complication.
+ */
+ private fun createComplicationQuad(
+ program: Gles2TexturedTriangleList.Program,
+ left: Float,
+ top: Float,
+ width: Float,
+ height: Float
+ ) = Gles2TexturedTriangleList(
+ program,
+ floatArrayOf(
+ top + 0.0f,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + height,
+ left + width,
+ 0.0f
+ ),
+ floatArrayOf(
+ 1.0f,
+ 1.0f,
+
+ 1.0f,
+ 0.0f,
+
+ 0.0f,
+ 1.0f,
+
+ 1.0f,
+ 0.0f,
+
+ 0.0f,
+ 1.0f,
+
+ 0.0f,
+ 0.0f
+ )
+ )
+
+ /**
+ * Creates a triangle list for the complication highlight quad.
+ */
+ private fun createComplicationHighlightQuad(
+ program: Gles2ColoredTriangleList.Program,
+ left: Float,
+ top: Float,
+ width: Float,
+ height: Float
+ ) = Gles2ColoredTriangleList(
+ program,
+ floatArrayOf(
+ top + 0.0f,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + 0.0f,
+ left + width,
+ 0.0f,
+
+ top + height,
+ left + 0.0f,
+ 0.0f,
+
+ top + height,
+ left + width,
+ 0.0f
+ ),
floatArrayOf(
1.0f /* red */,
1.0f /* green */,
1.0f /* blue */,
- 1.0f /* alpha */
+ 0.0f /* alpha */
)
)
- }
- /**
- * Creates a triangle list for the minor ticks on the watch face.
- *
- * @param program program for drawing triangles
- */
- private fun createMinorTicks(
- program: Gles2ColoredTriangleList.Program
- ): Gles2ColoredTriangleList {
- // Create the data for the VBO.
- val trianglesCoords = FloatArray(9 * (12 - 4))
- var index = 0
- for (i in 0..11) {
- if (i % 3 == 0) {
- // This is where a major tick goes, so skip it.
- continue
+ private fun getMajorTickTriangleCoords(index: Int): FloatArray {
+ return getTickTriangleCoords(
+ 0.03f /* width */, 0.09f /* length */,
+ index * 360 / 4 /* angleDegrees */
+ )
+ }
+
+ private fun getMinorTickTriangleCoords(index: Int): FloatArray {
+ return getTickTriangleCoords(
+ 0.02f /* width */, 0.06f /* length */,
+ index * 360 / 12 /* angleDegrees */
+ )
+ }
+
+ private fun getTickTriangleCoords(
+ width: Float,
+ length: Float,
+ angleDegrees: Int
+ ): FloatArray {
+ // Create the data for the VBO.
+ val coords = floatArrayOf(
+ 0f,
+ 1f,
+ 0f,
+ width / 2,
+ length + 1,
+ 0f,
+ -width / 2,
+ length + 1,
+ 0f
+ )
+ rotateCoords(coords, angleDegrees)
+ return coords
+ }
+
+ /**
+ * Destructively rotates the given coordinates in the XY plane about the origin by the given
+ * angle.
+ *
+ * @param coords flattened 3D coordinates
+ * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
+ * Z axis
+ */
+ private fun rotateCoords(
+ coords: FloatArray,
+ angleDegrees: Int
+ ) {
+ val angleRadians = Math.toRadians(angleDegrees.toDouble())
+ val cos = cos(angleRadians)
+ val sin = sin(angleRadians)
+ var i = 0
+ while (i < coords.size) {
+ val x = coords[i]
+ val y = coords[i + 1]
+ coords[i] = (cos * x - sin * y).toFloat()
+ coords[i + 1] = (sin * x + cos * y).toFloat()
+ i += 3
}
- val triangleCoords = getMinorTickTriangleCoords(i)
- System.arraycopy(
- triangleCoords,
- 0,
- trianglesCoords,
- index,
- triangleCoords.size
- )
- index += 9
}
- return Gles2ColoredTriangleList(
- program, trianglesCoords,
- floatArrayOf(
- 0.5f /* red */,
- 0.5f /* green */,
- 0.5f /* blue */,
- 1.0f /* alpha */
- )
- )
- }
- /**
- * Creates a triangle list for the complication.
- */
- private fun createComplicationQuad(
- program: Gles2TexturedTriangleList.Program,
- left: Float,
- top: Float,
- width: Float,
- height: Float
- ) = Gles2TexturedTriangleList(
- program,
- floatArrayOf(
- top + 0.0f,
- left + 0.0f,
- 0.0f,
-
- top + 0.0f,
- left + width,
- 0.0f,
-
- top + height,
- left + 0.0f,
- 0.0f,
-
- top + 0.0f,
- left + width,
- 0.0f,
-
- top + height,
- left + 0.0f,
- 0.0f,
-
- top + height,
- left + width,
- 0.0f
- ),
- floatArrayOf(
- 1.0f,
- 1.0f,
-
- 1.0f,
- 0.0f,
-
- 0.0f,
- 1.0f,
-
- 1.0f,
- 0.0f,
-
- 0.0f,
- 1.0f,
-
- 0.0f,
- 0.0f
- )
- )
-
- /**
- * Creates a triangle list for the complication highlight quad.
- */
- private fun createComplicationHighlightQuad(
- program: Gles2ColoredTriangleList.Program,
- left: Float,
- top: Float,
- width: Float,
- height: Float
- ) = Gles2ColoredTriangleList(
- program,
- floatArrayOf(
- top + 0.0f,
- left + 0.0f,
- 0.0f,
-
- top + 0.0f,
- left + width,
- 0.0f,
-
- top + height,
- left + 0.0f,
- 0.0f,
-
- top + 0.0f,
- left + width,
- 0.0f,
-
- top + height,
- left + 0.0f,
- 0.0f,
-
- top + height,
- left + width,
- 0.0f
- ),
- floatArrayOf(
- 1.0f /* red */,
- 1.0f /* green */,
- 1.0f /* blue */,
- 0.0f /* alpha */
- )
- )
-
- private fun getMajorTickTriangleCoords(index: Int): FloatArray {
- return getTickTriangleCoords(
- 0.03f /* width */, 0.09f /* length */,
- index * 360 / 4 /* angleDegrees */
- )
- }
-
- private fun getMinorTickTriangleCoords(index: Int): FloatArray {
- return getTickTriangleCoords(
- 0.02f /* width */, 0.06f /* length */,
- index * 360 / 12 /* angleDegrees */
- )
- }
-
- private fun getTickTriangleCoords(
- width: Float,
- length: Float,
- angleDegrees: Int
- ): FloatArray {
- // Create the data for the VBO.
- val coords = floatArrayOf(
- 0f,
- 1f,
- 0f,
- width / 2,
- length + 1,
- 0f,
- -width / 2,
- length + 1,
- 0f
- )
- rotateCoords(coords, angleDegrees)
- return coords
- }
-
- /**
- * Destructively rotates the given coordinates in the XY plane about the origin by the given
- * angle.
- *
- * @param coords flattened 3D coordinates
- * @param angleDegrees angle in degrees clockwise when viewed from negative infinity on the
- * Z axis
- */
- private fun rotateCoords(
- coords: FloatArray,
- angleDegrees: Int
- ) {
- val angleRadians = Math.toRadians(angleDegrees.toDouble())
- val cos = cos(angleRadians)
- val sin = sin(angleRadians)
- var i = 0
- while (i < coords.size) {
- val x = coords[i]
- val y = coords[i + 1]
- coords[i] = (cos * x - sin * y).toFloat()
- coords[i + 1] = (sin * x + cos * y).toFloat()
- i += 3
- }
- }
-
- override fun render(zonedDateTime: ZonedDateTime) {
- // Draw background color and select the appropriate view projection matrix. The background
- // should always be black in ambient mode. The view projection matrix used is overhead in
- // ambient. In interactive mode, it's tilted depending on the current time.
- val vpMatrix = if (renderParameters.drawMode == DrawMode.AMBIENT) {
- GLES20.glClearColor(0f, 0f, 0f, 1f)
- ambientVpMatrix
- } else {
- when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
- "red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
- "green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
+ override fun render(zonedDateTime: ZonedDateTime) {
+ // Draw background color and select the appropriate view projection matrix. The background
+ // should always be black in ambient mode. The view projection matrix used is overhead in
+ // ambient. In interactive mode, it's tilted depending on the current time.
+ val vpMatrix = if (renderParameters.drawMode == DrawMode.AMBIENT) {
+ GLES20.glClearColor(0f, 0f, 0f, 1f)
+ ambientVpMatrix
+ } else {
+ when (currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()) {
+ "red_style" -> GLES20.glClearColor(0.5f, 0.2f, 0.2f, 1f)
+ "green_style" -> GLES20.glClearColor(0.2f, 0.5f, 0.2f, 1f)
+ }
+ val cameraIndex =
+ (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles)
+ .toInt()
+ vpMatrices[cameraIndex]
}
- val cameraIndex =
- (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles)
- .toInt()
- vpMatrices[cameraIndex]
- }
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
- // Draw the complication first.
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
- complicationTexture.renderToTexture(zonedDateTime, renderParameters)
+ // Draw the complication first.
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+ complicationTexture.renderToTexture(zonedDateTime, renderParameters)
- textureTriangleProgram.bindProgramAndAttribs()
- complicationTexture.bind()
+ textureTriangleProgram.bindProgramAndAttribs()
+ complicationTexture.bind()
- GLES20.glEnable(GLES20.GL_BLEND)
- GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE)
- complicationTriangles.draw(vpMatrix)
- textureTriangleProgram.unbindAttribs()
+ GLES20.glEnable(GLES20.GL_BLEND)
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE)
+ complicationTriangles.draw(vpMatrix)
+ textureTriangleProgram.unbindAttribs()
- GLES20.glDisable(GLES20.GL_BLEND)
- GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
- coloredTriangleProgram.bindProgramAndAttribs()
- }
+ GLES20.glDisable(GLES20.GL_BLEND)
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+ coloredTriangleProgram.bindProgramAndAttribs()
+ }
- // Compute angle indices for the three hands.
- val seconds = zonedDateTime.second + (zonedDateTime.nano / 1000000000.0)
- val minutes = zonedDateTime.minute + seconds / 60f
- val hours = (zonedDateTime.hour % 12) + minutes / 60f
- val secIndex = (seconds / 60f * 360f).toInt()
- val minIndex = (minutes / 60f * 360f).toInt()
- val hoursIndex = (hours / 12f * 360f).toInt()
+ // Compute angle indices for the three hands.
+ val seconds = zonedDateTime.second + (zonedDateTime.nano / 1000000000.0)
+ val minutes = zonedDateTime.minute + seconds / 60f
+ val hours = (zonedDateTime.hour % 12) + minutes / 60f
+ val secIndex = (seconds / 60f * 360f).toInt()
+ val minIndex = (minutes / 60f * 360f).toInt()
+ val hoursIndex = (hours / 12f * 360f).toInt()
- // Render hands.
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)) {
- Matrix.multiplyMM(
- mvpMatrix,
- 0,
- vpMatrix,
- 0,
- modelMatrices[hoursIndex],
- 0
- )
- hourHandTriangle.draw(mvpMatrix)
-
- Matrix.multiplyMM(
- mvpMatrix,
- 0,
- vpMatrix,
- 0,
- modelMatrices[minIndex],
- 0
- )
- minuteHandTriangle.draw(mvpMatrix)
-
- if (renderParameters.drawMode != DrawMode.AMBIENT) {
+ // Render hands.
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS_OVERLAY)) {
Matrix.multiplyMM(
mvpMatrix,
0,
vpMatrix,
0,
- modelMatrices[secIndex],
+ modelMatrices[hoursIndex],
0
)
- secondHandTriangleMap[
- currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()
- ]?.draw(mvpMatrix)
- }
- }
+ hourHandTriangle.draw(mvpMatrix)
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
- majorTickTriangles.draw(vpMatrix)
- minorTickTriangles.draw(vpMatrix)
- coloredTriangleProgram.unbindAttribs()
- }
- }
-
- override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
- val cameraIndex =
- (zonedDateTime.toInstant().toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles).toInt()
- val vpMatrix = vpMatrices[cameraIndex]
-
- val highlightLayer = renderParameters.highlightLayer!!
- GLES20.glClearColor(
- Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
- Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
- )
- GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
-
- if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
- GLES20.glEnable(GLES20.GL_BLEND)
- GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
- complicationHighlightTriangles.draw(vpMatrix)
- coloredTriangleProgram.unbindAttribs()
- }
- }
-}
-
-/**
- * A list of triangles drawn in a single solid color using OpenGL ES 2.0.
- */
-class Gles2ColoredTriangleList(
- private val program: Program,
- triangleCoords: FloatArray,
- private val color: FloatArray
-) {
- init {
- require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
- ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
- }
- require(color.size == NUM_COLOR_COMPONENTS) { "wrong number of color components" }
- }
-
- /** The VBO containing the vertex coordinates. */
- private val vertexBuffer =
- ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
- .apply { order(ByteOrder.nativeOrder()) }
- .asFloatBuffer().apply {
- put(triangleCoords)
- position(0)
- }
-
- /** Number of coordinates in this triangle list. */
- private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
-
- /**
- * Draws this triangle list using OpenGL commands.
- *
- * @param mvpMatrix the Model View Project matrix to draw this triangle list
- */
- fun draw(mvpMatrix: FloatArray?) {
- // Pass the MVP matrix, vertex data, and color to OpenGL.
- program.bind(mvpMatrix, vertexBuffer, color)
-
- // Draw the triangle list.
- GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
- if (CHECK_GL_ERRORS) checkGlError(
- "glDrawArrays"
- )
- }
-
- /** OpenGL shaders for drawing solid colored triangle lists. */
- class Program {
- /** ID OpenGL uses to identify this program. */
- private val programId: Int
-
- /** Handle for uMvpMatrix uniform in vertex shader. */
- private val mvpMatrixHandle: Int
-
- /** Handle for aPosition attribute in vertex shader. */
- private val positionHandle: Int
-
- /** Handle for uColor uniform in fragment shader. */
- private val colorHandle: Int
-
- companion object {
- /** Trivial vertex shader that transforms the input vertex by the MVP matrix. */
- private const val VERTEX_SHADER_CODE = "" +
- "uniform mat4 uMvpMatrix;\n" +
- "attribute vec4 aPosition;\n" +
- "void main() {\n" +
- " gl_Position = uMvpMatrix * aPosition;\n" +
- "}\n"
-
- /** Trivial fragment shader that draws with a fixed color. */
- private const val FRAGMENT_SHADER_CODE = "" +
- "precision mediump float;\n" +
- "uniform vec4 uColor;\n" +
- "void main() {\n" +
- " gl_FragColor = uColor;\n" +
- "}\n"
- }
-
- /**
- * Tells OpenGL to use this program. Call this method before drawing a sequence of
- * triangle lists.
- */
- fun bindProgramAndAttribs() {
- GLES20.glUseProgram(programId)
- if (CHECK_GL_ERRORS) {
- checkGlError("glUseProgram")
- }
-
- // Enable vertex array (VBO).
- GLES20.glEnableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glEnableVertexAttribArray")
- }
- }
-
- fun unbindAttribs() {
- GLES20.glDisableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glDisableVertexAttribArray")
- }
- }
-
- /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
- fun bind(
- mvpMatrix: FloatArray?,
- vertexBuffer: FloatBuffer?,
- color: FloatArray?
- ) {
- // Pass the MVP matrix to OpenGL.
- GLES20.glUniformMatrix4fv(
- mvpMatrixHandle, 1 /* count */, false /* transpose */,
- mvpMatrix, 0 /* offset */
- )
- if (CHECK_GL_ERRORS) {
- checkGlError("glUniformMatrix4fv")
- }
-
- // Pass the VBO with the triangle list's vertices to OpenGL.
- GLES20.glEnableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glEnableVertexAttribArray")
- }
- GLES20.glVertexAttribPointer(
- positionHandle,
- COORDS_PER_VERTEX,
- GLES20.GL_FLOAT,
- false /* normalized */,
- VERTEX_STRIDE,
- vertexBuffer
- )
- if (CHECK_GL_ERRORS) {
- checkGlError("glVertexAttribPointer")
- }
-
- // Pass the triangle list's color to OpenGL.
- GLES20.glUniform4fv(colorHandle, 1 /* count */, color, 0 /* offset */)
- if (CHECK_GL_ERRORS) {
- checkGlError("glUniform4fv")
- }
- }
-
- /**
- * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
- * should be used for all triangle lists being drawn.
- */
- init {
- // Prepare shaders.
- val vertexShader = loadShader(
- GLES20.GL_VERTEX_SHADER,
- VERTEX_SHADER_CODE
- )
- val fragmentShader = loadShader(
- GLES20.GL_FRAGMENT_SHADER,
- FRAGMENT_SHADER_CODE
- )
-
- // Create empty OpenGL Program.
- programId = GLES20.glCreateProgram()
- if (CHECK_GL_ERRORS) checkGlError(
- "glCreateProgram"
- )
- check(programId != 0) { "glCreateProgram failed" }
-
- // Add the shaders to the program.
- GLES20.glAttachShader(programId, vertexShader)
- if (CHECK_GL_ERRORS) {
- checkGlError("glAttachShader")
- }
- GLES20.glAttachShader(programId, fragmentShader)
- if (CHECK_GL_ERRORS) {
- checkGlError("glAttachShader")
- }
-
- // Link the program so it can be executed.
- GLES20.glLinkProgram(programId)
- if (CHECK_GL_ERRORS) {
- checkGlError("glLinkProgram")
- }
-
- // Get a handle to the uMvpMatrix uniform in the vertex shader.
- mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetUniformLocation")
- }
-
- // Get a handle to the vertex shader's aPosition attribute.
- positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetAttribLocation")
- }
-
- // Get a handle to fragment shader's uColor uniform.
- colorHandle = GLES20.glGetUniformLocation(programId, "uColor")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetUniformLocation")
- }
- }
- }
-
- companion object {
- private const val TAG = "GlColoredTriangleList"
-
- /** Whether to check for GL errors. This is slow, so not appropriate for production builds. */
- private const val CHECK_GL_ERRORS = false
-
- /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
- private const val COORDS_PER_VERTEX = 3
-
- /** Number of bytes to store a float in GL. */
- const val BYTES_PER_FLOAT = 4
-
- /** Number of bytes per vertex. */
- private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
- /** Triangles have three vertices. */
- private const val VERTICES_PER_TRIANGLE = 3
-
- /**
- * Number of components in an OpenGL color. The components are:
- * 1. red
- * 1. green
- * 1. blue
- * 1. alpha
- *
- */
- private const val NUM_COLOR_COMPONENTS = 4
-
- /**
- * Checks if any of the GL calls since the last time this method was called set an error
- * condition. Call this method immediately after calling a GL method. Pass the name of the GL
- * operation. For example:
- *
- * <pre>
- * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
- * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
- *
- * If the operation is not successful, the check throws an exception.
- *
- *
- * *Note* This is quite slow so it's best to use it sparingly in production builds.
- *
- * @param glOperation name of the OpenGL call to check
- */
- internal fun checkGlError(glOperation: String) {
- val error = GLES20.glGetError()
- if (error != GLES20.GL_NO_ERROR) {
- var errorString = GLU.gluErrorString(error)
- if (errorString == null) {
- errorString = GLUtils.getEGLErrorString(error)
- }
- val message =
- glOperation + " caused GL error 0x" + Integer.toHexString(error) +
- ": " + errorString
- Log.e(TAG, message)
- throw RuntimeException(message)
- }
- }
-
- /**
- * Compiles an OpenGL shader.
- *
- * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
- * @param shaderCode string containing the shader source code
- * @return ID for the shader
- */
- internal fun loadShader(type: Int, shaderCode: String): Int {
- // Create a vertex or fragment shader.
- val shader = GLES20.glCreateShader(type)
- if (CHECK_GL_ERRORS) checkGlError(
- "glCreateShader"
- )
- check(shader != 0) { "glCreateShader failed" }
-
- // Add the source code to the shader and compile it.
- GLES20.glShaderSource(shader, shaderCode)
- if (CHECK_GL_ERRORS) checkGlError(
- "glShaderSource"
- )
- GLES20.glCompileShader(shader)
- if (CHECK_GL_ERRORS) checkGlError(
- "glCompileShader"
- )
- return shader
- }
- }
-}
-
-/**
- * A list of triangles drawn with a texture using OpenGL ES 2.0.
- */
-class Gles2TexturedTriangleList(
- private val program: Program,
- triangleCoords: FloatArray,
- private val textureCoords: FloatArray
-) {
- init {
- require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
- ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
- }
- require(textureCoords.size % (VERTICES_PER_TRIANGLE * TEXTURE_COORDS_PER_VERTEX) == 0) {
- (
- "must be multiple of VERTICES_PER_TRIANGLE * NUM_TEXTURE_COMPONENTS texture " +
- "coordinates"
+ Matrix.multiplyMM(
+ mvpMatrix,
+ 0,
+ vpMatrix,
+ 0,
+ modelMatrices[minIndex],
+ 0
)
- }
- }
+ minuteHandTriangle.draw(mvpMatrix)
- /** The VBO containing the vertex coordinates. */
- private val vertexBuffer =
- ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
- .apply { order(ByteOrder.nativeOrder()) }
- .asFloatBuffer().apply {
- put(triangleCoords)
- position(0)
+ if (renderParameters.drawMode != DrawMode.AMBIENT) {
+ Matrix.multiplyMM(
+ mvpMatrix,
+ 0,
+ vpMatrix,
+ 0,
+ modelMatrices[secIndex],
+ 0
+ )
+ secondHandTriangleMap[
+ currentUserStyleRepository.userStyle.value[colorStyleSetting]!!.toString()
+ ]?.draw(mvpMatrix)
+ }
}
- /** The VBO containing the vertex coordinates. */
- private val textureCoordsBuffer =
- ByteBuffer.allocateDirect(textureCoords.size * BYTES_PER_FLOAT)
- .apply { order(ByteOrder.nativeOrder()) }
- .asFloatBuffer().apply {
- put(textureCoords)
- position(0)
- }
-
- /** Number of coordinates in this triangle list. */
- private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
-
- /**
- * Draws this triangle list using OpenGL commands.
- *
- * @param mvpMatrix the Model View Project matrix to draw this triangle list
- */
- fun draw(mvpMatrix: FloatArray?) {
- // Pass the MVP matrix, vertex data, and color to OpenGL.
- program.bind(mvpMatrix, vertexBuffer, textureCoordsBuffer)
-
- // Draw the triangle list.
- GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
- if (CHECK_GL_ERRORS) checkGlError(
- "glDrawArrays"
- )
- }
-
- /** OpenGL shaders for drawing textured triangle lists. */
- class Program {
- /** ID OpenGL uses to identify this program. */
- private val programId: Int
-
- /** Handle for uMvpMatrix uniform in vertex shader. */
- private val mvpMatrixHandle: Int
-
- /** Handle for aPosition attribute in vertex shader. */
- private val positionHandle: Int
-
- /** Handle for aTextureCoordinate uniform in fragment shader. */
- private val textureCoordinateHandle: Int
-
- companion object {
- /** Trivial vertex shader that transforms the input vertex by the MVP matrix. */
- private const val VERTEX_SHADER_CODE = "" +
- "uniform mat4 uMvpMatrix;\n" +
- "attribute vec4 aPosition;\n" +
- "attribute vec4 aTextureCoordinate;\n" +
- "varying vec2 textureCoordinate;\n" +
- "void main() {\n" +
- " gl_Position = uMvpMatrix * aPosition;\n" +
- " textureCoordinate = aTextureCoordinate.xy;" +
- "}\n"
-
- /** Trivial fragment shader that draws with a texture. */
- private const val FRAGMENT_SHADER_CODE = "" +
- "varying highp vec2 textureCoordinate;\n" +
- "uniform sampler2D texture;\n" +
- "void main() {\n" +
- " gl_FragColor = texture2D(texture, textureCoordinate);\n" +
- "}\n"
- }
-
- /**
- * Tells OpenGL to use this program. Call this method before drawing a sequence of
- * triangle lists.
- */
- fun bindProgramAndAttribs() {
- GLES20.glUseProgram(programId)
- if (CHECK_GL_ERRORS) {
- checkGlError("glUseProgram")
- }
-
- // Enable vertex array (VBO).
- GLES20.glEnableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glEnableVertexAttribArray")
- }
-
- GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glEnableVertexAttribArray")
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) {
+ majorTickTriangles.draw(vpMatrix)
+ minorTickTriangles.draw(vpMatrix)
+ coloredTriangleProgram.unbindAttribs()
}
}
- fun unbindAttribs() {
- GLES20.glDisableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glDisableVertexAttribArray")
- }
+ override fun renderHighlightLayer(zonedDateTime: ZonedDateTime) {
+ val cameraIndex =
+ (zonedDateTime.toInstant()
+ .toEpochMilli() / FRAME_PERIOD_MS % numCameraAngles).toInt()
+ val vpMatrix = vpMatrices[cameraIndex]
- GLES20.glDisableVertexAttribArray(textureCoordinateHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glDisableVertexAttribArray")
- }
- }
-
- fun onDestroy() {
- GLES20.glDeleteProgram(programId)
- if (CHECK_GL_ERRORS) {
- checkGlError("glDeleteProgram")
- }
- }
-
- /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
- fun bind(
- mvpMatrix: FloatArray?,
- vertexBuffer: FloatBuffer?,
- textureCoordinatesBuffer: FloatBuffer?
- ) {
- // Pass the MVP matrix to OpenGL.
- GLES20.glUniformMatrix4fv(
- mvpMatrixHandle, 1 /* count */, false /* transpose */,
- mvpMatrix, 0 /* offset */
+ val highlightLayer = renderParameters.highlightLayer!!
+ GLES20.glClearColor(
+ Color.red(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.green(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.blue(highlightLayer.backgroundTint).toFloat() / 256.0f,
+ Color.alpha(highlightLayer.backgroundTint).toFloat() / 256.0f
)
- if (CHECK_GL_ERRORS) {
- checkGlError("glUniformMatrix4fv")
- }
+ GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
- // Pass the VBO with the triangle list's vertices to OpenGL.
- GLES20.glVertexAttribPointer(
- positionHandle,
- COORDS_PER_VERTEX,
- GLES20.GL_FLOAT,
- false /* normalized */,
- VERTEX_STRIDE,
- vertexBuffer
- )
- if (CHECK_GL_ERRORS) {
- checkGlError("glVertexAttribPointer")
- }
-
- // Pass the VBO with the triangle list's texture coordinates to OpenGL.
- GLES20.glVertexAttribPointer(
- textureCoordinateHandle,
- TEXTURE_COORDS_PER_VERTEX,
- GLES20.GL_FLOAT,
- false /* normalized */,
- TEXTURE_COORDS_VERTEX_STRIDE,
- textureCoordinatesBuffer
- )
- if (CHECK_GL_ERRORS) {
- checkGlError("glVertexAttribPointer")
- }
- }
-
- /**
- * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
- * should be used for all triangle lists being drawn.
- */
- init {
- // Prepare shaders.
- val vertexShader = loadShader(
- GLES20.GL_VERTEX_SHADER,
- VERTEX_SHADER_CODE
- )
- val fragmentShader = loadShader(
- GLES20.GL_FRAGMENT_SHADER,
- FRAGMENT_SHADER_CODE
- )
-
- // Create empty OpenGL Program.
- programId = GLES20.glCreateProgram()
- if (CHECK_GL_ERRORS) checkGlError(
- "glCreateProgram"
- )
- check(programId != 0) { "glCreateProgram failed" }
-
- // Add the shaders to the program.
- GLES20.glAttachShader(programId, vertexShader)
- if (CHECK_GL_ERRORS) {
- checkGlError("glAttachShader")
- }
- GLES20.glAttachShader(programId, fragmentShader)
- if (CHECK_GL_ERRORS) {
- checkGlError("glAttachShader")
- }
-
- // Link the program so it can be executed.
- GLES20.glLinkProgram(programId)
- if (CHECK_GL_ERRORS) {
- checkGlError("glLinkProgram")
- }
-
- // Get a handle to the uMvpMatrix uniform in the vertex shader.
- mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetUniformLocation")
- }
-
- // Get a handle to the vertex shader's aPosition attribute.
- positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetAttribLocation")
- }
-
- // Get a handle to vertex shader's aUV attribute.
- textureCoordinateHandle =
- GLES20.glGetAttribLocation(programId, "aTextureCoordinate")
- if (CHECK_GL_ERRORS) {
- checkGlError("glGetAttribLocation")
- }
-
- // Enable vertex array (VBO).
- GLES20.glEnableVertexAttribArray(positionHandle)
- if (CHECK_GL_ERRORS) {
- checkGlError("glEnableVertexAttribArray")
+ if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS)) {
+ GLES20.glEnable(GLES20.GL_BLEND)
+ GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ZERO)
+ complicationHighlightTriangles.draw(vpMatrix)
+ coloredTriangleProgram.unbindAttribs()
}
}
}
companion object {
- private const val TAG = "Gles2TexturedTriangleList"
+ /** Expected frame rate in interactive mode. */
+ private const val FPS: Long = 60
- /**
- * Whether to check for GL errors. This is slow, so not appropriate for production builds.
- */
- private const val CHECK_GL_ERRORS = false
+ /** Z distance from the camera to the watchface. */
+ private const val EYE_Z = -2.3f
- /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
- private const val COORDS_PER_VERTEX = 3
+ /** How long each frame is displayed at expected frame rate. */
+ private const val FRAME_PERIOD_MS: Long = 1000 / FPS
- /** Number of texture coordinates per vertex in this array: one for u & v */
- private const val TEXTURE_COORDS_PER_VERTEX = 2
+ /** Cycle time before the camera motion repeats. */
+ private const val CYCLE_PERIOD_SECONDS: Long = 5
- /** Number of bytes to store a float in GL. */
- const val BYTES_PER_FLOAT = 4
+ /** Number of camera angles to precompute. */
+ private const val numCameraAngles = (CYCLE_PERIOD_SECONDS * FPS).toInt()
- /** Number of bytes per vertex. */
- private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
- /** Number of bytes per vertex for texture coords. */
- private const val TEXTURE_COORDS_VERTEX_STRIDE = TEXTURE_COORDS_PER_VERTEX * BYTES_PER_FLOAT
-
- /** Triangles have three vertices. */
- private const val VERTICES_PER_TRIANGLE = 3
-
- /**
- * Checks if any of the GL calls since the last time this method was called set an error
- * condition. Call this method immediately after calling a GL method. Pass the name of the
- * GL operation. For example:
- *
- * <pre>
- * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
- * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
- *
- * If the operation is not successful, the check throws an exception.
- *
- *
- * *Note* This is quite slow so it's best to use it sparingly in production builds.
- *
- * @param glOperation name of the OpenGL call to check
- */
- internal fun checkGlError(glOperation: String) {
- val error = GLES20.glGetError()
- if (error != GLES20.GL_NO_ERROR) {
- var errorString = GLU.gluErrorString(error)
- if (errorString == null) {
- errorString = GLUtils.getEGLErrorString(error)
- }
- val message =
- glOperation + " caused GL error 0x" + Integer.toHexString(error) +
- ": " + errorString
- Log.e(TAG, message)
- throw RuntimeException(message)
- }
- }
-
- /**
- * Compiles an OpenGL shader.
- *
- * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
- * @param shaderCode string containing the shader source code
- * @return ID for the shader
- */
- internal fun loadShader(type: Int, shaderCode: String): Int {
- // Create a vertex or fragment shader.
- val shader = GLES20.glCreateShader(type)
- if (CHECK_GL_ERRORS) checkGlError(
- "glCreateShader"
- )
- check(shader != 0) { "glCreateShader failed" }
-
- // Add the source code to the shader and compile it.
- GLES20.glShaderSource(shader, shaderCode)
- if (CHECK_GL_ERRORS) checkGlError(
- "glShaderSource"
- )
- GLES20.glCompileShader(shader)
- if (CHECK_GL_ERRORS) checkGlError(
- "glCompileShader"
- )
- return shader
- }
+ const val EXAMPLE_OPENGL_COMPLICATION_ID = 101
}
-}
\ No newline at end of file
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt
new file mode 100644
index 0000000..21dcca8
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2ColoredTriangleList.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.samples
+
+import android.opengl.GLES20
+import android.opengl.GLU
+import android.opengl.GLUtils
+import android.util.Log
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+
+/**
+ * A list of triangles drawn in a single solid color using OpenGL ES 2.0.
+ */
+internal class Gles2ColoredTriangleList(
+ private val program: Program,
+ triangleCoords: FloatArray,
+ private val color: FloatArray
+) {
+ init {
+ require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
+ ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
+ }
+ require(color.size == NUM_COLOR_COMPONENTS) { "wrong number of color components" }
+ }
+
+ /** The VBO containing the vertex coordinates. */
+ private val vertexBuffer =
+ ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
+ .apply { order(ByteOrder.nativeOrder()) }
+ .asFloatBuffer().apply {
+ put(triangleCoords)
+ position(0)
+ }
+
+ /** Number of coordinates in this triangle list. */
+ private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
+
+ /**
+ * Draws this triangle list using OpenGL commands.
+ *
+ * @param mvpMatrix the Model View Project matrix to draw this triangle list
+ */
+ fun draw(mvpMatrix: FloatArray?) {
+ // Pass the MVP matrix, vertex data, and color to OpenGL.
+ program.bind(mvpMatrix, vertexBuffer, color)
+
+ // Draw the triangle list.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glDrawArrays"
+ )
+ }
+
+ /** OpenGL shaders for drawing solid colored triangle lists. */
+ class Program {
+ /** ID OpenGL uses to identify this program. */
+ private val programId: Int
+
+ /** Handle for uMvpMatrix uniform in vertex shader. */
+ private val mvpMatrixHandle: Int
+
+ /** Handle for aPosition attribute in vertex shader. */
+ private val positionHandle: Int
+
+ /** Handle for uColor uniform in fragment shader. */
+ private val colorHandle: Int
+
+ companion object {
+ /** Trivial vertex shader that transforms the input vertex by the MVP matrix. */
+ private const val VERTEX_SHADER_CODE = "" +
+ "uniform mat4 uMvpMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "void main() {\n" +
+ " gl_Position = uMvpMatrix * aPosition;\n" +
+ "}\n"
+
+ /** Trivial fragment shader that draws with a fixed color. */
+ private const val FRAGMENT_SHADER_CODE = "" +
+ "precision mediump float;\n" +
+ "uniform vec4 uColor;\n" +
+ "void main() {\n" +
+ " gl_FragColor = uColor;\n" +
+ "}\n"
+ }
+
+ /**
+ * Tells OpenGL to use this program. Call this method before drawing a sequence of
+ * triangle lists.
+ */
+ fun bindProgramAndAttribs() {
+ GLES20.glUseProgram(programId)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glUseProgram")
+ }
+
+ // Enable vertex array (VBO).
+ GLES20.glEnableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glEnableVertexAttribArray")
+ }
+ }
+
+ fun unbindAttribs() {
+ GLES20.glDisableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glDisableVertexAttribArray")
+ }
+ }
+
+ /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
+ fun bind(
+ mvpMatrix: FloatArray?,
+ vertexBuffer: FloatBuffer?,
+ color: FloatArray?
+ ) {
+ // Pass the MVP matrix to OpenGL.
+ GLES20.glUniformMatrix4fv(
+ mvpMatrixHandle, 1 /* count */, false /* transpose */,
+ mvpMatrix, 0 /* offset */
+ )
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glUniformMatrix4fv")
+ }
+
+ // Pass the VBO with the triangle list's vertices to OpenGL.
+ GLES20.glEnableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glEnableVertexAttribArray")
+ }
+ GLES20.glVertexAttribPointer(
+ positionHandle,
+ COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT,
+ false /* normalized */,
+ VERTEX_STRIDE,
+ vertexBuffer
+ )
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glVertexAttribPointer")
+ }
+
+ // Pass the triangle list's color to OpenGL.
+ GLES20.glUniform4fv(colorHandle, 1 /* count */, color, 0 /* offset */)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glUniform4fv")
+ }
+ }
+
+ /**
+ * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
+ * should be used for all triangle lists being drawn.
+ */
+ init {
+ // Prepare shaders.
+ val vertexShader = loadShader(
+ GLES20.GL_VERTEX_SHADER,
+ VERTEX_SHADER_CODE
+ )
+ val fragmentShader = loadShader(
+ GLES20.GL_FRAGMENT_SHADER,
+ FRAGMENT_SHADER_CODE
+ )
+
+ // Create empty OpenGL Program.
+ programId = GLES20.glCreateProgram()
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCreateProgram"
+ )
+ check(programId != 0) { "glCreateProgram failed" }
+
+ // Add the shaders to the program.
+ GLES20.glAttachShader(programId, vertexShader)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glAttachShader")
+ }
+ GLES20.glAttachShader(programId, fragmentShader)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glAttachShader")
+ }
+
+ // Link the program so it can be executed.
+ GLES20.glLinkProgram(programId)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glLinkProgram")
+ }
+
+ // Get a handle to the uMvpMatrix uniform in the vertex shader.
+ mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetUniformLocation")
+ }
+
+ // Get a handle to the vertex shader's aPosition attribute.
+ positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetAttribLocation")
+ }
+
+ // Get a handle to fragment shader's uColor uniform.
+ colorHandle = GLES20.glGetUniformLocation(programId, "uColor")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetUniformLocation")
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "GlColoredTriangleList"
+
+ /** Whether to check for GL errors. This is slow, so not appropriate for production builds. */
+ private const val CHECK_GL_ERRORS = false
+
+ /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
+ private const val COORDS_PER_VERTEX = 3
+
+ /** Number of bytes to store a float in GL. */
+ const val BYTES_PER_FLOAT = 4
+
+ /** Number of bytes per vertex. */
+ private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+ /** Triangles have three vertices. */
+ private const val VERTICES_PER_TRIANGLE = 3
+
+ /**
+ * Number of components in an OpenGL color. The components are:
+ * 1. red
+ * 1. green
+ * 1. blue
+ * 1. alpha
+ *
+ */
+ private const val NUM_COLOR_COMPONENTS = 4
+
+ /**
+ * Checks if any of the GL calls since the last time this method was called set an error
+ * condition. Call this method immediately after calling a GL method. Pass the name of the GL
+ * operation. For example:
+ *
+ * <pre>
+ * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+ *
+ * If the operation is not successful, the check throws an exception.
+ *
+ *
+ * *Note* This is quite slow so it's best to use it sparingly in production builds.
+ *
+ * @param glOperation name of the OpenGL call to check
+ */
+ internal fun checkGlError(glOperation: String) {
+ val error = GLES20.glGetError()
+ if (error != GLES20.GL_NO_ERROR) {
+ var errorString = GLU.gluErrorString(error)
+ if (errorString == null) {
+ errorString = GLUtils.getEGLErrorString(error)
+ }
+ val message =
+ glOperation + " caused GL error 0x" + Integer.toHexString(error) +
+ ": " + errorString
+ Log.e(TAG, message)
+ throw RuntimeException(message)
+ }
+ }
+
+ /**
+ * Compiles an OpenGL shader.
+ *
+ * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
+ * @param shaderCode string containing the shader source code
+ * @return ID for the shader
+ */
+ internal fun loadShader(type: Int, shaderCode: String): Int {
+ // Create a vertex or fragment shader.
+ val shader = GLES20.glCreateShader(type)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCreateShader"
+ )
+ check(shader != 0) { "glCreateShader failed" }
+
+ // Add the source code to the shader and compile it.
+ GLES20.glShaderSource(shader, shaderCode)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glShaderSource"
+ )
+ GLES20.glCompileShader(shader)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCompileShader"
+ )
+ return shader
+ }
+ }
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt
new file mode 100644
index 0000000..0161406
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/Gles2TexturedTriangleList.kt
@@ -0,0 +1,352 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.watchface.samples
+
+import android.opengl.GLES20
+import android.opengl.GLU
+import android.opengl.GLUtils
+import android.util.Log
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.nio.FloatBuffer
+
+/**
+ * A list of triangles drawn with a texture using OpenGL ES 2.0.
+ */
+internal class Gles2TexturedTriangleList(
+ private val program: Program,
+ triangleCoords: FloatArray,
+ private val textureCoords: FloatArray
+) {
+ init {
+ require(triangleCoords.size % (VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX) == 0) {
+ ("must be multiple of VERTICES_PER_TRIANGLE * COORDS_PER_VERTEX coordinates")
+ }
+ require(textureCoords.size % (VERTICES_PER_TRIANGLE * TEXTURE_COORDS_PER_VERTEX) == 0) {
+ (
+ "must be multiple of VERTICES_PER_TRIANGLE * NUM_TEXTURE_COMPONENTS texture " +
+ "coordinates"
+ )
+ }
+ }
+
+ /** The VBO containing the vertex coordinates. */
+ private val vertexBuffer =
+ ByteBuffer.allocateDirect(triangleCoords.size * BYTES_PER_FLOAT)
+ .apply { order(ByteOrder.nativeOrder()) }
+ .asFloatBuffer().apply {
+ put(triangleCoords)
+ position(0)
+ }
+
+ /** The VBO containing the vertex coordinates. */
+ private val textureCoordsBuffer =
+ ByteBuffer.allocateDirect(textureCoords.size * BYTES_PER_FLOAT)
+ .apply { order(ByteOrder.nativeOrder()) }
+ .asFloatBuffer().apply {
+ put(textureCoords)
+ position(0)
+ }
+
+ /** Number of coordinates in this triangle list. */
+ private val numCoords = triangleCoords.size / COORDS_PER_VERTEX
+
+ /**
+ * Draws this triangle list using OpenGL commands.
+ *
+ * @param mvpMatrix the Model View Project matrix to draw this triangle list
+ */
+ fun draw(mvpMatrix: FloatArray?) {
+ // Pass the MVP matrix, vertex data, and color to OpenGL.
+ program.bind(mvpMatrix, vertexBuffer, textureCoordsBuffer)
+
+ // Draw the triangle list.
+ GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, numCoords)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glDrawArrays"
+ )
+ }
+
+ /** OpenGL shaders for drawing textured triangle lists. */
+ class Program {
+ /** ID OpenGL uses to identify this program. */
+ private val programId: Int
+
+ /** Handle for uMvpMatrix uniform in vertex shader. */
+ private val mvpMatrixHandle: Int
+
+ /** Handle for aPosition attribute in vertex shader. */
+ private val positionHandle: Int
+
+ /** Handle for aTextureCoordinate uniform in fragment shader. */
+ private val textureCoordinateHandle: Int
+
+ companion object {
+ /** Trivial vertex shader that transforms the input vertex by the MVP matrix. */
+ private const val VERTEX_SHADER_CODE = "" +
+ "uniform mat4 uMvpMatrix;\n" +
+ "attribute vec4 aPosition;\n" +
+ "attribute vec4 aTextureCoordinate;\n" +
+ "varying vec2 textureCoordinate;\n" +
+ "void main() {\n" +
+ " gl_Position = uMvpMatrix * aPosition;\n" +
+ " textureCoordinate = aTextureCoordinate.xy;" +
+ "}\n"
+
+ /** Trivial fragment shader that draws with a texture. */
+ private const val FRAGMENT_SHADER_CODE = "" +
+ "varying highp vec2 textureCoordinate;\n" +
+ "uniform sampler2D texture;\n" +
+ "void main() {\n" +
+ " gl_FragColor = texture2D(texture, textureCoordinate);\n" +
+ "}\n"
+ }
+
+ /**
+ * Tells OpenGL to use this program. Call this method before drawing a sequence of
+ * triangle lists.
+ */
+ fun bindProgramAndAttribs() {
+ GLES20.glUseProgram(programId)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glUseProgram")
+ }
+
+ // Enable vertex array (VBO).
+ GLES20.glEnableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glEnableVertexAttribArray")
+ }
+
+ GLES20.glEnableVertexAttribArray(textureCoordinateHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glEnableVertexAttribArray")
+ }
+ }
+
+ fun unbindAttribs() {
+ GLES20.glDisableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glDisableVertexAttribArray")
+ }
+
+ GLES20.glDisableVertexAttribArray(textureCoordinateHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glDisableVertexAttribArray")
+ }
+ }
+
+ fun onDestroy() {
+ GLES20.glDeleteProgram(programId)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glDeleteProgram")
+ }
+ }
+
+ /** Sends the given MVP matrix, vertex data, and color to OpenGL. */
+ fun bind(
+ mvpMatrix: FloatArray?,
+ vertexBuffer: FloatBuffer?,
+ textureCoordinatesBuffer: FloatBuffer?
+ ) {
+ // Pass the MVP matrix to OpenGL.
+ GLES20.glUniformMatrix4fv(
+ mvpMatrixHandle, 1 /* count */, false /* transpose */,
+ mvpMatrix, 0 /* offset */
+ )
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glUniformMatrix4fv")
+ }
+
+ // Pass the VBO with the triangle list's vertices to OpenGL.
+ GLES20.glVertexAttribPointer(
+ positionHandle,
+ COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT,
+ false /* normalized */,
+ VERTEX_STRIDE,
+ vertexBuffer
+ )
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glVertexAttribPointer")
+ }
+
+ // Pass the VBO with the triangle list's texture coordinates to OpenGL.
+ GLES20.glVertexAttribPointer(
+ textureCoordinateHandle,
+ TEXTURE_COORDS_PER_VERTEX,
+ GLES20.GL_FLOAT,
+ false /* normalized */,
+ TEXTURE_COORDS_VERTEX_STRIDE,
+ textureCoordinatesBuffer
+ )
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glVertexAttribPointer")
+ }
+ }
+
+ /**
+ * Creates a program to draw triangle lists. For optimal drawing efficiency, one program
+ * should be used for all triangle lists being drawn.
+ */
+ init {
+ // Prepare shaders.
+ val vertexShader = loadShader(
+ GLES20.GL_VERTEX_SHADER,
+ VERTEX_SHADER_CODE
+ )
+ val fragmentShader = loadShader(
+ GLES20.GL_FRAGMENT_SHADER,
+ FRAGMENT_SHADER_CODE
+ )
+
+ // Create empty OpenGL Program.
+ programId = GLES20.glCreateProgram()
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCreateProgram"
+ )
+ check(programId != 0) { "glCreateProgram failed" }
+
+ // Add the shaders to the program.
+ GLES20.glAttachShader(programId, vertexShader)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glAttachShader")
+ }
+ GLES20.glAttachShader(programId, fragmentShader)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glAttachShader")
+ }
+
+ // Link the program so it can be executed.
+ GLES20.glLinkProgram(programId)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glLinkProgram")
+ }
+
+ // Get a handle to the uMvpMatrix uniform in the vertex shader.
+ mvpMatrixHandle = GLES20.glGetUniformLocation(programId, "uMvpMatrix")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetUniformLocation")
+ }
+
+ // Get a handle to the vertex shader's aPosition attribute.
+ positionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetAttribLocation")
+ }
+
+ // Get a handle to vertex shader's aUV attribute.
+ textureCoordinateHandle =
+ GLES20.glGetAttribLocation(programId, "aTextureCoordinate")
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glGetAttribLocation")
+ }
+
+ // Enable vertex array (VBO).
+ GLES20.glEnableVertexAttribArray(positionHandle)
+ if (CHECK_GL_ERRORS) {
+ checkGlError("glEnableVertexAttribArray")
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "Gles2TexturedTriangleList"
+
+ /**
+ * Whether to check for GL errors. This is slow, so not appropriate for production builds.
+ */
+ private const val CHECK_GL_ERRORS = false
+
+ /** Number of coordinates per vertex in this array: one for each of x, y, and z. */
+ private const val COORDS_PER_VERTEX = 3
+
+ /** Number of texture coordinates per vertex in this array: one for u & v */
+ private const val TEXTURE_COORDS_PER_VERTEX = 2
+
+ /** Number of bytes to store a float in GL. */
+ const val BYTES_PER_FLOAT = 4
+
+ /** Number of bytes per vertex. */
+ private const val VERTEX_STRIDE = COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+ /** Number of bytes per vertex for texture coords. */
+ private const val TEXTURE_COORDS_VERTEX_STRIDE =
+ TEXTURE_COORDS_PER_VERTEX * BYTES_PER_FLOAT
+
+ /** Triangles have three vertices. */
+ private const val VERTICES_PER_TRIANGLE = 3
+
+ /**
+ * Checks if any of the GL calls since the last time this method was called set an error
+ * condition. Call this method immediately after calling a GL method. Pass the name of the
+ * GL operation. For example:
+ *
+ * <pre>
+ * mColorHandle = GLES20.glGetUniformLocation(mProgram, "uColor");
+ * MyGLRenderer.checkGlError("glGetUniformLocation");</pre>
+ *
+ * If the operation is not successful, the check throws an exception.
+ *
+ *
+ * *Note* This is quite slow so it's best to use it sparingly in production builds.
+ *
+ * @param glOperation name of the OpenGL call to check
+ */
+ internal fun checkGlError(glOperation: String) {
+ val error = GLES20.glGetError()
+ if (error != GLES20.GL_NO_ERROR) {
+ var errorString = GLU.gluErrorString(error)
+ if (errorString == null) {
+ errorString = GLUtils.getEGLErrorString(error)
+ }
+ val message =
+ glOperation + " caused GL error 0x" + Integer.toHexString(error) +
+ ": " + errorString
+ Log.e(TAG, message)
+ throw RuntimeException(message)
+ }
+ }
+
+ /**
+ * Compiles an OpenGL shader.
+ *
+ * @param type [GLES20.GL_VERTEX_SHADER] or [GLES20.GL_FRAGMENT_SHADER]
+ * @param shaderCode string containing the shader source code
+ * @return ID for the shader
+ */
+ internal fun loadShader(type: Int, shaderCode: String): Int {
+ // Create a vertex or fragment shader.
+ val shader = GLES20.glCreateShader(type)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCreateShader"
+ )
+ check(shader != 0) { "glCreateShader failed" }
+
+ // Add the source code to the shader and compile it.
+ GLES20.glShaderSource(shader, shaderCode)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glShaderSource"
+ )
+ GLES20.glCompileShader(shader)
+ if (CHECK_GL_ERRORS) checkGlError(
+ "glCompileShader"
+ )
+ return shader
+ }
+ }
+}
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index ca0c213..c2404ff 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -23,10 +23,6 @@
import android.graphics.RectF
import android.view.SurfaceHolder
import androidx.annotation.Sampled
-import androidx.wear.watchface.complications.ComplicationSlotBounds
-import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
-import androidx.wear.watchface.complications.SystemDataSources
-import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.CanvasComplicationFactory
import androidx.wear.watchface.CanvasType
import androidx.wear.watchface.ComplicationSlot
@@ -36,6 +32,10 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
import androidx.wear.watchface.complications.rendering.ComplicationDrawable
import androidx.wear.watchface.style.CurrentUserStyleRepository
@@ -44,11 +44,20 @@
import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting
import androidx.wear.watchface.style.UserStyleSetting.Option
import androidx.wear.watchface.style.WatchFaceLayer
+import java.time.ZonedDateTime
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
-import java.time.ZonedDateTime
+
+private const val COLOR_STYLE_SETTING = "color_style_setting"
+private const val RED_STYLE = "red_style"
+private const val GREEN_STYLE = "green_style"
+private const val BLUE_STYLE = "blue_style"
+
+private const val HAND_STYLE_SETTING = "hand_style_setting"
+private const val CLASSIC_STYLE = "classic_style"
+private const val MODERN_STYLE = "modern_style"
+private const val GOTHIC_STYLE = "gothic_style"
@Sampled
fun kDocCreateExampleWatchFaceService(): WatchFaceService {
@@ -57,24 +66,28 @@
UserStyleSchema(
listOf(
ListUserStyleSetting(
- UserStyleSetting.Id("color_style_setting"),
- "Colors",
- "Watchface colorization",
+ UserStyleSetting.Id(COLOR_STYLE_SETTING),
+ resources,
+ R.string.colors_style_setting,
+ R.string.colors_style_setting_description,
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- Option.Id("red_style"),
- "Red",
+ Option.Id(RED_STYLE),
+ resources,
+ R.string.colors_style_red,
icon = null
),
ListUserStyleSetting.ListOption(
- Option.Id("green_style"),
- "Green",
+ Option.Id(GREEN_STYLE),
+ resources,
+ R.string.colors_style_green,
icon = null
),
ListUserStyleSetting.ListOption(
- Option.Id("blue_style"),
- "Blue",
+ Option.Id(BLUE_STYLE),
+ resources,
+ R.string.colors_style_blue,
icon = null
)
),
@@ -85,20 +98,28 @@
)
),
ListUserStyleSetting(
- UserStyleSetting.Id("hand_style_setting"),
- "Hand Style",
- "Hand visual look",
+ UserStyleSetting.Id(HAND_STYLE_SETTING),
+ resources,
+ R.string.hand_style_setting,
+ R.string.hand_style_setting_description,
icon = null,
options = listOf(
ListUserStyleSetting.ListOption(
- Option.Id("classic_style"), "Classic", icon = null
+ Option.Id(CLASSIC_STYLE),
+ resources,
+ R.string.hand_style_classic,
+ icon = null
),
ListUserStyleSetting.ListOption(
- Option.Id("modern_style"), "Modern", icon = null
+ Option.Id(MODERN_STYLE),
+ resources,
+ R.string.hand_style_modern,
+ icon = null
),
ListUserStyleSetting.ListOption(
- Option.Id("gothic_style"),
- "Gothic",
+ Option.Id(GOTHIC_STYLE),
+ resources,
+ R.string.hand_style_gothic,
icon = null
)
),
diff --git a/wear/watchface/watchface/samples/src/main/res/values/strings.xml b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
index 776c74e..a5d1d53 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -54,6 +54,21 @@
<!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
<string name="colors_style_yellow">Yellow</string>
+ <!-- Name of watchface style category for selecting the hand style [CHAR LIMIT=20] -->
+ <string name="hand_style_setting" translatable="false">Hand Style</string>
+
+ <!-- Subtitle for the menu option to select the watch face hand style [CHAR LIMIT=20] -->
+ <string name="hand_style_setting_description" translatable="false">Hand visual look</string>
+
+ <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+ <string name="hand_style_classic" translatable="false">Classic</string>
+
+ <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+ <string name="hand_style_modern" translatable="false">Modern</string>
+
+ <!-- An option with the watch face hand style settings [CHAR LIMIT=20] -->
+ <string name="hand_style_gothic" translatable="false">Gothic</string>
+
<!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
<string name="watchface_pips_setting">Hour Pips</string>
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
index 0d7e7cb..8084e48 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceControlServiceTest.kt
@@ -20,6 +20,7 @@
import android.content.Context
import android.content.Intent
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import android.support.wearable.watchface.SharedMemoryImage
@@ -42,7 +43,11 @@
import androidx.wear.watchface.WatchFaceService
import androidx.wear.watchface.WatchFaceType
import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.data.ColorRamp
+import androidx.wear.watchface.complications.data.GoalProgressComplicationData
import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.RangedValueTypes
+import androidx.wear.watchface.complications.data.WeightedElementsComplicationData
import androidx.wear.watchface.control.IHeadlessWatchFace
import androidx.wear.watchface.control.IWatchFaceControlService
import androidx.wear.watchface.control.WatchFaceControlService
@@ -51,11 +56,11 @@
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
-import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
-import androidx.wear.watchface.samples.EXAMPLE_OPENGL_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService
+import androidx.wear.watchface.samples.ExampleOpenGLWatchFaceService.Companion.EXAMPLE_OPENGL_COMPLICATION_ID
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.WatchFaceLayer
import com.google.common.truth.Truth.assertThat
@@ -68,6 +73,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import java.time.ZonedDateTime
+import org.junit.After
// This service constructs a WatchFace with a task that's posted on the UI thread.
internal class AsyncInitWithUiThreadTaskWatchFace : WatchFaceService() {
@@ -101,267 +107,408 @@
}
}
+const val TIME_MILLIS: Long = 123456789
+val DEVICE_CONFIG = DeviceConfig(
+ /* hasLowBitAmbient = */ false,
+ /* hasBurnInProtection = */ false,
+ /* analogPreviewReferenceTimeMillis = */ 0,
+ /* digitalPreviewReferenceTimeMillis = */ 0
+)
+
@RunWith(AndroidJUnit4::class)
@RequiresApi(Build.VERSION_CODES.O_MR1)
@MediumTest
public class WatchFaceControlServiceTest {
- @get:Rule
- internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
+ @get:Rule
+ internal val screenshotRule = AndroidXScreenshotTestRule("wear/wear-watchface")
- @Before
- public fun setUp() {
- Assume.assumeTrue("This test suite assumes API 27", Build.VERSION.SDK_INT >= 27)
- }
+ private lateinit var instance: IHeadlessWatchFace
- private fun createInstance(width: Int, height: Int): IHeadlessWatchFace {
- val instanceService = IWatchFaceControlService.Stub.asInterface(
- WatchFaceControlService().apply {
- setContext(ApplicationProvider.getApplicationContext<Context>())
- }.onBind(
- Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
- )
- )
- return instanceService.createHeadlessWatchFaceInstance(
- HeadlessWatchFaceInstanceParams(
- ComponentName(
- ApplicationProvider.getApplicationContext<Context>(),
- ExampleCanvasAnalogWatchFaceService::class.java
- ),
- DeviceConfig(
- false,
- false,
- 0,
- 0
- ),
- width,
- height,
- null
- )
- )
- }
+ @Before
+ public fun setUp() {
+ Assume.assumeTrue("This test suite assumes API 27", Build.VERSION.SDK_INT >= 27)
+ }
- private fun createOpenGlInstance(width: Int, height: Int): IHeadlessWatchFace {
- val instanceService = IWatchFaceControlService.Stub.asInterface(
- WatchFaceControlService().apply {
- setContext(ApplicationProvider.getApplicationContext<Context>())
- }.onBind(
- Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
- )
- )
- return instanceService.createHeadlessWatchFaceInstance(
- HeadlessWatchFaceInstanceParams(
- ComponentName(
- ApplicationProvider.getApplicationContext<Context>(),
- ExampleOpenGLWatchFaceService::class.java
- ),
- DeviceConfig(
- false,
- false,
- 0,
- 0
- ),
- width,
- height,
- null
- )
- )
- }
+ @After
+ public fun tearDown() {
+ if (this::instance.isInitialized) {
+ instance.release()
+ }
+ }
- @Test
- public fun createWatchFaceInstanceWithRangedValueComplications() {
- val instance = createInstance(400, 400)
- try {
- val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.renderWatchFaceToBitmap(
- WatchFaceRenderParams(
- RenderParameters(
- DrawMode.INTERACTIVE,
- WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
- null
- ).toWireFormat(),
- 1234567890,
- null,
- listOf(
- IdAndComplicationDataWireFormat(
- EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
- RangedValueComplicationData.Builder(
- 100.0f, 0.0f, 100.0f,
- ComplicationText.EMPTY
- )
- .setText(PlainComplicationText.Builder("100%").build())
- .build()
- .asWireComplicationData()
- ),
- IdAndComplicationDataWireFormat(
- EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
- RangedValueComplicationData.Builder(
- 75.0f, 0.0f, 100.0f,
- ComplicationText.EMPTY
- )
- .setText(PlainComplicationText.Builder("75%").build())
- .build()
- .asWireComplicationData()
- )
- )
- )
- )
- )
+ private fun createInstance(width: Int, height: Int) {
+ val instanceService = IWatchFaceControlService.Stub.asInterface(
+ WatchFaceControlService().apply {
+ setContext(ApplicationProvider.getApplicationContext<Context>())
+ }.onBind(
+ Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+ )
+ )
+ instance = instanceService.createHeadlessWatchFaceInstance(
+ HeadlessWatchFaceInstanceParams(
+ ComponentName(
+ ApplicationProvider.getApplicationContext<Context>(),
+ ExampleCanvasAnalogWatchFaceService::class.java
+ ),
+ DEVICE_CONFIG,
+ width,
+ height,
+ null
+ )
+ )
+ }
- bitmap.assertAgainstGolden(screenshotRule, "ranged_value_complications")
- } finally {
- instance.release()
- }
- }
+ private fun createOpenGlInstance(width: Int, height: Int) {
+ val instanceService = IWatchFaceControlService.Stub.asInterface(
+ WatchFaceControlService().apply {
+ setContext(ApplicationProvider.getApplicationContext<Context>())
+ }.onBind(
+ Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+ )
+ )
+ instance = instanceService.createHeadlessWatchFaceInstance(
+ HeadlessWatchFaceInstanceParams(
+ ComponentName(
+ ApplicationProvider.getApplicationContext<Context>(),
+ ExampleOpenGLWatchFaceService::class.java
+ ),
+ DEVICE_CONFIG,
+ width,
+ height,
+ null
+ )
+ )
+ }
- @Test
- public fun createHeadlessWatchFaceInstance() {
- val instance = createInstance(100, 100)
- try {
- val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.renderWatchFaceToBitmap(
- WatchFaceRenderParams(
- RenderParameters(
- DrawMode.INTERACTIVE,
- WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
- null
- ).toWireFormat(),
- 1234567890,
- null,
- listOf(
- IdAndComplicationDataWireFormat(
- EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
- ShortTextComplicationData.Builder(
- PlainComplicationText.Builder("Mon").build(),
- ComplicationText.EMPTY
- )
- .setTitle(PlainComplicationText.Builder("23rd").build())
- .build()
- .asWireComplicationData()
- ),
- IdAndComplicationDataWireFormat(
- EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
- ShortTextComplicationData.Builder(
- PlainComplicationText.Builder("100").build(),
- ComplicationText.EMPTY
- )
- .setTitle(PlainComplicationText.Builder("Steps").build())
- .build()
- .asWireComplicationData()
- )
- )
- )
- )
- )
+ @Test
+ public fun createWatchFaceInstanceWithRangedValueComplications() {
+ createInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null
+ ).toWireFormat(),
+ TIME_MILLIS,
+ null,
+ listOf(
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RangedValueComplicationData.Builder(
+ value = 100.0f,
+ min = 0.0f,
+ max = 100.0f,
+ ComplicationText.EMPTY
+ )
+ .setText(PlainComplicationText.Builder("100%").build())
+ .build()
+ .asWireComplicationData()
+ ),
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+ RangedValueComplicationData.Builder(
+ value = 75.0f,
+ min = 0.0f,
+ max = 100.0f,
+ ComplicationText.EMPTY
+ )
+ .setText(PlainComplicationText.Builder("75%").build())
+ .build()
+ .asWireComplicationData()
+ )
+ )
+ )
+ )
+ )
- bitmap.assertAgainstGolden(screenshotRule, "service_interactive")
- } finally {
- instance.release()
- }
- }
+ bitmap.assertAgainstGolden(screenshotRule, "ranged_value_complications")
+ }
- @Test
- public fun createHeadlessOpenglWatchFaceInstance() {
- val instance = createOpenGlInstance(400, 400)
- try {
- val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.renderWatchFaceToBitmap(
- WatchFaceRenderParams(
- RenderParameters(
- DrawMode.INTERACTIVE,
- WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
- null
- ).toWireFormat(),
- 1234567890,
- null,
- listOf(
- IdAndComplicationDataWireFormat(
- EXAMPLE_OPENGL_COMPLICATION_ID,
- ShortTextComplicationData.Builder(
- PlainComplicationText.Builder("Mon").build(),
- ComplicationText.EMPTY
- )
- .setTitle(PlainComplicationText.Builder("23rd").build())
- .build()
- .asWireComplicationData()
- )
- )
- )
- )
- )
+ @Test
+ public fun createHeadlessWatchFaceInstance() {
+ createInstance(width = 100, height = 100)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null
+ ).toWireFormat(),
+ TIME_MILLIS,
+ null,
+ listOf(
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("Mon").build(),
+ ComplicationText.EMPTY
+ )
+ .setTitle(PlainComplicationText.Builder("23rd").build())
+ .build()
+ .asWireComplicationData()
+ ),
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("100").build(),
+ ComplicationText.EMPTY
+ )
+ .setTitle(PlainComplicationText.Builder("Steps").build())
+ .build()
+ .asWireComplicationData()
+ )
+ )
+ )
+ )
+ )
- bitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
- } finally {
- instance.release()
- }
- }
+ bitmap.assertAgainstGolden(screenshotRule, "service_interactive")
+ }
- @Test
- public fun testCommandTakeComplicationScreenShot() {
- val instance = createInstance(400, 400)
- try {
- val bitmap = SharedMemoryImage.ashmemReadImageBundle(
- instance.renderComplicationToBitmap(
- ComplicationRenderParams(
- EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
- RenderParameters(
- DrawMode.INTERACTIVE,
- WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
- null,
- ).toWireFormat(),
- 123456789,
- ShortTextComplicationData.Builder(
- PlainComplicationText.Builder("Mon").build(),
- ComplicationText.EMPTY
- )
- .setTitle(PlainComplicationText.Builder("23rd").build())
- .build()
- .asWireComplicationData(),
- null
- )
- )
- )
+ @Test
+ public fun createHeadlessOpenglWatchFaceInstance() {
+ createOpenGlInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderWatchFaceToBitmap(
+ WatchFaceRenderParams(
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null
+ ).toWireFormat(),
+ TIME_MILLIS,
+ null,
+ listOf(
+ IdAndComplicationDataWireFormat(
+ EXAMPLE_OPENGL_COMPLICATION_ID,
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("Mon").build(),
+ ComplicationText.EMPTY
+ )
+ .setTitle(PlainComplicationText.Builder("23rd").build())
+ .build()
+ .asWireComplicationData()
+ )
+ )
+ )
+ )
+ )
- bitmap.assertAgainstGolden(
- screenshotRule,
- "leftComplication"
- )
- } finally {
- instance.release()
- }
- }
+ bitmap.assertAgainstGolden(screenshotRule, "opengl_headless")
+ }
- @Test
- public fun asyncInitWithUiThreadTaskWatchFace() {
- val instanceService = IWatchFaceControlService.Stub.asInterface(
- WatchFaceControlService().apply {
- setContext(ApplicationProvider.getApplicationContext<Context>())
- }.onBind(
- Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
- )
- )
- // This shouldn't hang.
- val headlessInstance = instanceService.createHeadlessWatchFaceInstance(
- HeadlessWatchFaceInstanceParams(
- ComponentName(
- ApplicationProvider.getApplicationContext<Context>(),
- AsyncInitWithUiThreadTaskWatchFace::class.java
- ),
- DeviceConfig(
- false,
- false,
- 0,
- 0
- ),
- 100,
- 100,
- null
- )
- )
- try {
- assertThat(headlessInstance.userStyleSchema.mSchema).isEmpty()
- } finally {
- headlessInstance.release()
- }
- }
+ @Test
+ public fun testCommandTakeComplicationScreenShot() {
+ createInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ ).toWireFormat(),
+ TIME_MILLIS,
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("Mon").build(),
+ ComplicationText.EMPTY
+ )
+ .setTitle(PlainComplicationText.Builder("23rd").build())
+ .build()
+ .asWireComplicationData(),
+ null
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(
+ screenshotRule,
+ "leftComplication"
+ )
+ }
+
+ @Test
+ @Suppress("NewApi")
+ public fun testGoalProgressComplication() {
+ createInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ ).toWireFormat(),
+ TIME_MILLIS,
+ GoalProgressComplicationData.Builder(
+ value = 12345.0f,
+ targetValue = 10000.0f,
+ PlainComplicationText.Builder("12345 steps").build()
+ ).setText(PlainComplicationText.Builder("12345").build())
+ .setTitle(PlainComplicationText.Builder("Steps").build())
+ .build()
+ .asWireComplicationData(),
+ null
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(
+ screenshotRule,
+ "goalProgressComplication"
+ )
+ }
+
+ @Test
+ public fun testColorRampRangedValueComplication() {
+ createInstance(400, 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ ).toWireFormat(),
+ TIME_MILLIS,
+ RangedValueComplicationData.Builder(
+ value = 75f,
+ min = 0.0f,
+ max = 100.0f,
+ PlainComplicationText.Builder("Rainbow colors").build()
+ ).setText(PlainComplicationText.Builder("Colors").build())
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(
+ Color.GREEN,
+ Color.YELLOW,
+ Color.argb(255, 255, 255, 0),
+ Color.RED,
+ Color.argb(255, 255, 0, 255),
+ Color.argb(255, 92, 64, 51)
+ ),
+ interpolated = true
+ )
+ )
+ .build()
+ .asWireComplicationData(),
+ null
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(
+ screenshotRule,
+ "colorRampRangedValueComplication"
+ )
+ }
+
+ @Test
+ public fun testNonInterpolatedColorRampRangedValueComplication() {
+ createInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ ).toWireFormat(),
+ TIME_MILLIS,
+ RangedValueComplicationData.Builder(
+ value = 75f,
+ min = 0.0f,
+ max = 100.0f,
+ PlainComplicationText.Builder("Rainbow colors").build()
+ ).setText(PlainComplicationText.Builder("Colors").build())
+ .setValueType(RangedValueTypes.SCORE)
+ .setColorRamp(
+ ColorRamp(
+ intArrayOf(Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW),
+ interpolated = false
+ )
+ )
+ .build()
+ .asWireComplicationData(),
+ null
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(
+ screenshotRule,
+ "nonInterpolatedColorRampRangedValueComplication"
+ )
+ }
+
+ @Test
+ @Suppress("NewApi")
+ public fun testWeightedElementComplication() {
+ createInstance(width = 400, height = 400)
+ val bitmap = SharedMemoryImage.ashmemReadImageBundle(
+ instance.renderComplicationToBitmap(
+ ComplicationRenderParams(
+ EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
+ RenderParameters(
+ DrawMode.INTERACTIVE,
+ WatchFaceLayer.ALL_WATCH_FACE_LAYERS,
+ null,
+ ).toWireFormat(),
+ TIME_MILLIS,
+ WeightedElementsComplicationData.Builder(
+ listOf(
+ WeightedElementsComplicationData.Element(weight = 1.0f, Color.RED),
+ WeightedElementsComplicationData.Element(weight = 1.0f, Color.GREEN),
+ WeightedElementsComplicationData.Element(weight = 2.0f, Color.BLUE),
+ WeightedElementsComplicationData.Element(weight = 3.0f, Color.YELLOW)
+ ),
+ PlainComplicationText.Builder("Example").build()
+ ).setText(PlainComplicationText.Builder("Calories").build())
+ .build()
+ .asWireComplicationData(),
+ null
+ )
+ )
+ )
+
+ bitmap.assertAgainstGolden(
+ screenshotRule,
+ "weightedElementComplication"
+ )
+ }
+
+ @Test
+ public fun asyncInitWithUiThreadTaskWatchFace() {
+ val instanceService = IWatchFaceControlService.Stub.asInterface(
+ WatchFaceControlService().apply {
+ setContext(ApplicationProvider.getApplicationContext<Context>())
+ }.onBind(
+ Intent(WatchFaceControlService.ACTION_WATCHFACE_CONTROL_SERVICE)
+ )
+ )
+ // This shouldn't hang.
+ instance = instanceService.createHeadlessWatchFaceInstance(
+ HeadlessWatchFaceInstanceParams(
+ ComponentName(
+ ApplicationProvider.getApplicationContext<Context>(),
+ AsyncInitWithUiThreadTaskWatchFace::class.java
+ ),
+ DEVICE_CONFIG,
+ /* width = */ 100,
+ /* height = */100,
+ /* instanceId = */null
+ )
+ )
+
+ assertThat(instance.userStyleSchema.mSchema).isEmpty()
+ }
}
diff --git a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index bab4cc7..548403e 100644
--- a/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/watchface/watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -70,10 +70,10 @@
import androidx.wear.watchface.data.DeviceConfig
import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
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
-import androidx.wear.watchface.samples.GREEN_STYLE
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.GREEN_STYLE
import androidx.wear.watchface.style.CurrentUserStyleRepository
import androidx.wear.watchface.style.UserStyleSchema
import androidx.wear.watchface.style.WatchFaceLayer
@@ -665,25 +665,25 @@
listOf(
IdAndComplicationDataWireFormat(
EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID,
- ShortTextComplicationData.Builder(
- PlainComplicationText.Builder("Test").build(),
- ComplicationText.EMPTY
- )
- .setSmallImage(smallImage)
- .build()
- .asWireComplicationData()
+ ShortTextComplicationData.Builder(
+ PlainComplicationText.Builder("Test").build(),
+ ComplicationText.EMPTY
+ )
+ .setSmallImage(smallImage)
+ .build()
+ .asWireComplicationData()
),
IdAndComplicationDataWireFormat(
EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID,
- RangedValueComplicationData.Builder(
- 25f,
- 0f,
- 100f,
- ComplicationText.EMPTY
- )
- .setSmallImage(smallImage)
- .build()
- .asWireComplicationData()
+ RangedValueComplicationData.Builder(
+ 25f,
+ 0f,
+ 100f,
+ ComplicationText.EMPTY
+ )
+ .setSmallImage(smallImage)
+ .build()
+ .asWireComplicationData()
)
)
)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index d557218..bd6657e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -374,7 +374,7 @@
@ComplicationSlotBoundsType public val boundsType: Int,
bounds: ComplicationSlotBounds,
public val canvasComplicationFactory: CanvasComplicationFactory,
- supportedTypes: List<ComplicationType>,
+ public val supportedTypes: List<ComplicationType>,
defaultPolicy: DefaultComplicationDataSourcePolicy,
defaultDataSourceType: ComplicationType,
@get:JvmName("isInitiallyEnabled")
@@ -861,12 +861,6 @@
enabledDirty = true
}
- /** The types of complicationSlots the complication supports. Must be non-empty. */
-
- public val supportedTypes: List<ComplicationType> = supportedTypes
- @UiThread // TODO(b/229727216): Remove this annotation.
- get
-
internal var defaultDataSourcePolicyDirty = true
/**
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 3916ab2..b5b5e6c 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -2662,7 +2662,10 @@
writer.println("WatchFaceEngine:")
writer.increaseIndent()
when {
- wslFlow.iWatchFaceServiceInitialized() -> writer.println("WSL style init flow")
+ wslFlow.iWatchFaceServiceInitialized() -> {
+ writer.println("WSL style init flow")
+ writer.println("watchFaceInitStarted=${wslFlow.watchFaceInitStarted}")
+ }
this.watchFaceCreatedOrPending() -> writer.println("Androidx style init flow")
isPreAndroidR() -> writer.println("Expecting WSL style init")
else -> writer.println("Expecting androidx style style init")
@@ -2681,7 +2684,6 @@
}
}
writer.println("createdBy=$createdBy")
- writer.println("watchFaceInitStarted=$wslFlow.watchFaceInitStarted")
writer.println("asyncWatchFaceConstructionPending=$asyncWatchFaceConstructionPending")
writer.println(
"systemViewOfContentDescriptionLabelsIsStale=" +
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index 11ef49f..b41c16a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -58,7 +58,7 @@
@VisibleForTesting
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class WatchFaceControlService : Service() {
- private val watchFaceInstanceServiceStub by lazy { createServiceStub() }
+ private var watchFaceInstanceServiceStub: IWatchFaceInstanceServiceStub? = null
/** @hide */
public companion object {
@@ -69,6 +69,9 @@
override fun onBind(intent: Intent?): IBinder? =
TraceEvent("WatchFaceControlService.onBind").use {
if (ACTION_WATCHFACE_CONTROL_SERVICE == intent?.action) {
+ if (watchFaceInstanceServiceStub == null) {
+ watchFaceInstanceServiceStub = createServiceStub()
+ }
watchFaceInstanceServiceStub
} else {
null
@@ -94,13 +97,19 @@
HeadlessWatchFaceImpl.dump(indentingPrintWriter)
indentingPrintWriter.flush()
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ watchFaceInstanceServiceStub?.onDestroy()
+ }
}
/** @hide */
@RequiresApi(27)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open class IWatchFaceInstanceServiceStub(
- private val service: Service,
+ // We need to explicitly null this object in onDestroy to avoid a memory leak.
+ private var service: Service?,
private val uiThreadCoroutineScope: CoroutineScope
) : IWatchFaceControlService.Stub() {
override fun getApiVersion(): Int = IWatchFaceControlService.API_VERSION
@@ -169,11 +178,11 @@
method!!.isAccessible = true
method.invoke(
watchFaceService,
- service as Context,
+ service!! as Context,
null,
watchFaceService::class.qualifiedName,
null,
- service.application,
+ service!!.application,
null
)
} catch (e: Exception) {
@@ -280,4 +289,8 @@
throw e
}
}
+
+ fun onDestroy() {
+ service = null
+ }
}
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 038d5ce..0bdb170 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -4071,6 +4071,7 @@
).isEqualTo("Example")
assertThat(engineWrapper.contentDescriptionLabels[1].tapAction).isEqualTo(pendingIntent)
+ engineWrapper.onDestroy()
}
@Test
@@ -4826,7 +4827,7 @@
}
@Test
- public fun dump() {
+ public fun dump_androidXFlow() {
// Advance time a little so timestamps are not zero
looperTimeMillis = 1000
@@ -4878,6 +4879,24 @@
assertThat(dump).contains("screenBounds=Rect(0, 0 - 100, 100)")
assertThat(dump).contains("interactiveDrawModeUpdateDelayMillis=16")
assertThat(dump).contains("watchFaceLayers=BASE, COMPLICATIONS, COMPLICATIONS_OVERLAY")
+ assertThat(dump).doesNotContain("watchFaceInitStarted=")
+ }
+
+ @Test
+ public fun dump_wslFlow() {
+ initEngine(
+ WatchFaceType.DIGITAL,
+ listOf(leftComplication, rightComplication),
+ UserStyleSchema(emptyList())
+ )
+
+ val writer = StringWriter()
+ val indentingPrintWriter = IndentingPrintWriter(writer)
+
+ engineWrapper.dump(indentingPrintWriter)
+
+ val dump = writer.toString()
+ assertThat(dump).contains("watchFaceInitStarted=true")
}
@Test
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
new file mode 100644
index 0000000..19549fb
--- /dev/null
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/control/data/WallpaperInteractiveWatchFaceInstanceParamsTest.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.wear.watchface.control.data
+
+import android.content.ComponentName
+import androidx.versionedparcelable.ParcelUtils
+import androidx.wear.watchface.data.DeviceConfig
+import androidx.wear.watchface.data.WatchUiState
+import androidx.wear.watchface.style.UserStyle
+import java.io.ByteArrayOutputStream
+import org.junit.Test
+
+class WallpaperInteractiveWatchFaceInstanceParamsTest {
+ @Test
+ fun canBeWrittenToOutputStream() {
+ val params = WallpaperInteractiveWatchFaceInstanceParams(
+ "instanceId",
+ DeviceConfig(
+ false,
+ false,
+ 10,
+ 10
+ ),
+ WatchUiState(
+ false,
+ 0,
+ ),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ ComponentName("some.package", "SomeClass")
+ )
+
+ val dummyOutputStream = ByteArrayOutputStream()
+
+ // Should not throw an exception
+ ParcelUtils.toOutputStream(params, dummyOutputStream)
+ }
+}
\ No newline at end of file
diff --git a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
index 4afca04..eaa4585 100644
--- a/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
+++ b/wear/wear-phone-interactions/src/test/java/androidx/wear/phone/interactions/PhoneTypeHelperTest.kt
@@ -50,7 +50,7 @@
@Suppress("DEPRECATION") // b/251211092
@Before
fun setUp() {
- MockitoAnnotations.initMocks(this)
+ MockitoAnnotations.openMocks(this)
ShadowContentResolver.registerProviderInternal(
PhoneTypeHelper.SETTINGS_AUTHORITY,
mockContentProvider
@@ -167,4 +167,4 @@
)
.thenReturn(createFakeBluetoothModeCursor(phoneType))
}
-}
\ No newline at end of file
+}
diff --git a/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
index f3a455a..47c60499 100644
--- a/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
+++ b/wear/wear/src/test/java/androidx/wear/utils/WearTypeHelperTest.java
@@ -41,7 +41,7 @@
@SuppressWarnings("deprecation") // b/251211092
@Before
public void setUp() {
- MockitoAnnotations.initMocks(this);
+ MockitoAnnotations.openMocks(this);
mContext = ApplicationProvider.getApplicationContext();
mShadowPackageManager = Shadows.shadowOf(mContext.getPackageManager());
}
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
new file mode 100644
index 0000000..1c4b8ce
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.webkit;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.core.content.ContextCompat;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.LargeTest;
+import androidx.webkit.WebViewFeature;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+/**
+ * Integration test for {@link ProcessGlobalConfigActivity}.
+ */
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ProcessGlobalConfigActivityTestAppTest {
+ @Rule
+ public ActivityScenarioRule<MainActivity> mRule =
+ new ActivityScenarioRule<>(MainActivity.class);
+
+ @Before
+ public void setUp() {
+ WebkitTestHelpers.assumeStartupFeature(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+ ApplicationProvider.getApplicationContext());
+ }
+
+ @Test
+ public void testSetDataDirectorySuffix() throws Throwable {
+ final String dataDirPrefix = "app_webview_";
+ final String dataDirSuffix = "per_process_webview_data_0";
+
+ WebkitTestHelpers.clickMenuListItemWithString(
+ R.string.process_global_config_activity_title);
+ Thread.sleep(1000);
+
+ File file = new File(ContextCompat.getDataDir(ApplicationProvider.getApplicationContext()),
+ dataDirPrefix + dataDirSuffix);
+
+ assertTrue(file.exists());
+ }
+}
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
index 28d12ab..7f5257d 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
@@ -16,11 +16,10 @@
package com.example.androidx.webkit;
+import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA;
-import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
@@ -29,8 +28,8 @@
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static org.hamcrest.Matchers.containsString;
-import static org.hamcrest.core.AllOf.allOf;
-import static org.hamcrest.core.Is.is;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasToString;
import android.content.Context;
@@ -54,8 +53,8 @@
* @param resourceId string id of menu item
*/
public static void clickMenuListItemWithString(@StringRes int resourceId) {
- onView(allOf(isDescendantOfA(withClassName(is(MenuListView.class.getName()))),
- withText(resourceId))).perform(click());
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ onData(hasToString(equalTo(context.getString(resourceId)))).perform(click());
}
/**
@@ -128,6 +127,39 @@
}
/**
+ * Throws {@link org.junit.AssumptionViolatedException} if the device does not support the
+ * particular startup feature, otherwise returns.
+ *
+ * <p>
+ * This provides a more descriptive message than a bare {@code assumeTrue} call.
+ *
+ * @param featureName the feature to be checked
+ */
+ public static void assumeStartupFeature(
+ @WebViewFeature.WebViewStartupFeature String featureName,
+ Context context) {
+ final String msg = "This device does not have the feature '" + featureName + "'";
+ final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+ Assume.assumeTrue(msg, hasFeature);
+ }
+
+ /**
+ * Throws {@link org.junit.AssumptionViolatedException} if the device supports the
+ * particular startup feature, otherwise returns.
+ *
+ * <p>
+ * This provides a more descriptive message than a bare {@code assumeFalse} call.
+ *
+ * @param featureName the feature to be checked
+ */
+ public static void assumeStartupFeatureNotAvailable(
+ @WebViewFeature.WebViewStartupFeature String featureName, Context context) {
+ final String msg = "This device has the feature '" + featureName + "'";
+ final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+ Assume.assumeFalse(msg, hasFeature);
+ }
+
+ /**
* Javascript has to be enabled for espresso tests to work.
*
* @param webViewIds WebView IDs for which to enable JavaScript
diff --git a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
index 74e90b23..0428b05 100644
--- a/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/webkit/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -123,12 +123,24 @@
<activity
android:name=".GetVariationsHeaderActivity"
android:exported="true" />
- <activity android:name=".ProcessGlobalConfigActivity"
+ <activity
+ android:name=".ProcessGlobalConfigActivity"
+ android:process=":processGlobalConfigTest"
android:exported="true" />
<activity
android:name=".RequestedWithHeaderActivity"
android:exported="true" />
<activity android:name=".CookieManagerActivity"
android:exported="true" />
+ <activity
+ android:name=".ImageDragActivity"
+ android:exported="false" />
+
+ <provider
+ android:authorities="com.example.androidx.webkit.DropDataProvider"
+ android:name="androidx.webkit.DropDataContentProvider"
+ android:exported="false"
+ android:grantUriPermissions="true"/>
+
</application>
</manifest>
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
new file mode 100644
index 0000000..1ae07c2
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ImageDragActivity.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.androidx.webkit;
+
+import android.os.Bundle;
+import android.webkit.WebView;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+/**
+ * An Activity to demonstrate example for dragging image out to other apps.
+ * You can use google logo to test.
+ */
+public class ImageDragActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_image_drag);
+ WebView demoWebview = findViewById(R.id.image_webview);
+
+ demoWebview.loadUrl("www.google.com");
+ }
+}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
index b5525525..bf8f85e 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/MainActivity.java
@@ -84,6 +84,9 @@
new MenuListView.MenuItem(
getResources().getString(R.string.cookie_manager_activity_title),
new Intent(activityContext, CookieManagerActivity.class)),
+ new MenuListView.MenuItem(
+ getResources().getString(R.string.image_drag_drop_activity_title),
+ new Intent(activityContext, ImageDragActivity.class)),
};
listView.setItems(menuItems);
}
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index d0080db..c74f667 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -35,14 +35,19 @@
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (WebViewFeature.isStartupFeatureSupported(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+ setTitle(R.string.process_global_config_activity_title);
+ WebkitHelpers.appendWebViewVersionToTitle(this);
+
+ if (!WebViewFeature.isStartupFeatureSupported(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
this)) {
- ProcessGlobalConfig.createInstance()
- .setDataDirectorySuffix("per_process_webview_data_0", this)
- .apply();
+ WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
+ return;
}
- WebView wv = new WebView(this);
- setContentView(wv);
+ ProcessGlobalConfig.createInstance()
+ .setDataDirectorySuffix("per_process_webview_data_0", this)
+ .apply();
+ setContentView(R.layout.activity_process_global_config);
+ WebView wv = findViewById(R.id.process_global_config_webview);
wv.setWebViewClient(new WebViewClient());
wv.loadUrl("www.google.com");
}
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml
new file mode 100644
index 0000000..dab2b9a
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_image_drag.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <WebView
+ android:id="@+id/image_webview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
new file mode 100644
index 0000000..c2717b2
--- /dev/null
+++ b/webkit/integration-tests/testapp/src/main/res/layout/activity_process_global_config.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<WebView
+xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/process_global_config_webview"
+android:layout_width="match_parent"
+android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 92d9e9c..fd3734d 100644
--- a/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/webkit/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -88,6 +88,7 @@
<string name="cookie_manager_old_api_text">Old cookie API: </string>
<string name="cookie_manager_new_api_text">New cookie API: </string>
<string name="cookie_manager_get_cookie_info_not_supported">CookieManagerCompat#getCookieInfo not supported.</string>
+ <string name="image_drag_drop_activity_title">Image Drag And Drop</string>
<!-- Proxy Override -->
<string name="proxy_override_activity_title">Proxy Override</string>
diff --git a/webkit/webkit/api/current.ignore b/webkit/webkit/api/current.ignore
new file mode 100644
index 0000000..2da110a
--- /dev/null
+++ b/webkit/webkit/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderOriginAllowList():
+ Added method androidx.webkit.ServiceWorkerWebSettingsCompat.getRequestedWithHeaderOriginAllowList()
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>):
+ Added method androidx.webkit.ServiceWorkerWebSettingsCompat.setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>)
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -65,10 +65,12 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
}
public class TracingConfig {
@@ -137,6 +139,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -65,10 +65,12 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
}
public class TracingConfig {
@@ -137,6 +139,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/api/restricted_current.ignore b/webkit/webkit/api/restricted_current.ignore
new file mode 100644
index 0000000..2da110a
--- /dev/null
+++ b/webkit/webkit/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#getRequestedWithHeaderOriginAllowList():
+ Added method androidx.webkit.ServiceWorkerWebSettingsCompat.getRequestedWithHeaderOriginAllowList()
+AddedAbstractMethod: androidx.webkit.ServiceWorkerWebSettingsCompat#setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>):
+ Added method androidx.webkit.ServiceWorkerWebSettingsCompat.setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String>)
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index c19a92e..772b183 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -65,10 +65,12 @@
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getAllowFileAccess();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract boolean getBlockNetworkLoads();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract int getCacheMode();
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList();
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowContentAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_FILE_ACCESS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setAllowFileAccess(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setBlockNetworkLoads(boolean);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SERVICE_WORKER_CACHE_MODE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setCacheMode(int);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void setRequestedWithHeaderOriginAllowList(java.util.Set<java.lang.String!>);
}
public class TracingConfig {
@@ -137,6 +139,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDark(android.webkit.WebSettings);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static int getForceDarkStrategy(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getOffscreenPreRaster(android.webkit.WebSettings);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static java.util.Set<java.lang.String!> getRequestedWithHeaderOriginAllowList(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean getSafeBrowsingEnabled(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static boolean isAlgorithmicDarkeningAllowed(android.webkit.WebSettings);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.ALGORITHMIC_DARKENING, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setAlgorithmicDarkeningAllowed(android.webkit.WebSettings, boolean);
@@ -145,6 +148,7 @@
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDark(android.webkit.WebSettings, int);
method @Deprecated @RequiresFeature(name=androidx.webkit.WebViewFeature.FORCE_DARK_STRATEGY, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setForceDarkStrategy(android.webkit.WebSettings, int);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.OFF_SCREEN_PRERASTER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setOffscreenPreRaster(android.webkit.WebSettings, boolean);
+ method @RequiresFeature(name="REQUESTED_WITH_HEADER_ALLOW_LIST", enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setRequestedWithHeaderOriginAllowList(android.webkit.WebSettings, java.util.Set<java.lang.String!>);
method @RequiresFeature(name=androidx.webkit.WebViewFeature.SAFE_BROWSING_ENABLE, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public static void setSafeBrowsingEnabled(android.webkit.WebSettings, boolean);
field @Deprecated public static final int DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2; // 0x2
field @Deprecated public static final int DARK_STRATEGY_USER_AGENT_DARKENING_ONLY = 0; // 0x0
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
index e86763f..c71942e 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
@@ -135,6 +135,7 @@
*/
@SuppressWarnings("deprecation")
@FlakyTest(bugId = 240432254)
+ @SdkSuppress(maxSdkVersion = 32) // b/254572377
@Test
public void testForceDark_webThemeDarkeningOnly() {
WebkitUtils.checkFeature(WebViewFeature.FORCE_DARK);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 4606dc4..f2a5a06 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -55,6 +55,7 @@
/**
* Test web content is always light regardless the algorithmic darkening is allowed or not.
*/
+ @SdkSuppress(maxSdkVersion = 32) // b/254572377
@Test
public void testSimplifiedDarkMode_rendersLight() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
@@ -79,6 +80,7 @@
/**
* Test web content is always light (if supported) on the light theme app.
*/
+ @SdkSuppress(maxSdkVersion = 32) // b/254572377
@Test
public void testSimplifiedDarkMode_pageSupportDarkTheme() {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
diff --git a/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
new file mode 100644
index 0000000..6c38760
--- /dev/null
+++ b/webkit/webkit/src/main/java/androidx/webkit/DropDataContentProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.webkit;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.webkit.internal.WebViewGlueCommunicator;
+
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
+
+import java.io.FileNotFoundException;
+
+/**
+ * TODO(1353048): Un-hide this after finishing the feature.
+ *
+ * @hide
+ * This should be added to the manifest in order to enable dragging images out.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class DropDataContentProvider extends ContentProvider {
+ DropDataContentProviderBoundaryInterface mImpl;
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Nullable
+ @Override
+ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
+ throws FileNotFoundException {
+ return getDropImpl().openFile(this, uri);
+ }
+
+ @Nullable
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
+ @Nullable String selection, @Nullable String[] selectionArgs,
+ @Nullable String sortOrder) {
+ return getDropImpl().query(uri, projection, selection, selectionArgs, sortOrder);
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return getDropImpl().getType(uri);
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
+ throw new UnsupportedOperationException("Insert method is not supported.");
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ throw new UnsupportedOperationException("delete method is not supported.");
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s,
+ @Nullable String[] strings) {
+ throw new UnsupportedOperationException("update method is not supported.");
+ }
+
+ @Nullable
+ @Override
+ public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
+ return getDropImpl().call(method, arg, extras);
+ }
+
+ private DropDataContentProviderBoundaryInterface getDropImpl() {
+ if (mImpl == null) {
+ mImpl = WebViewGlueCommunicator.getFactory().getDropDataProvider();
+ mImpl.onCreate();
+ }
+ return mImpl;
+ }
+}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
index 692fec2..ae5ac6a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ServiceWorkerWebSettingsCompat.java
@@ -184,13 +184,14 @@
* <p>
* Any origin <em>not</em> on this allow-list may not receive the header, depending on the
* current installed WebView provider.
+ * <p>
+ * The format of the strings in the allow-list follows the origin rules of
+ * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
*
* @return The configured set of allow-listed origins.
* @see #setRequestedWithHeaderOriginAllowList(Set)
* @see WebSettingsCompat#getRequestedWithHeaderOriginAllowList(WebSettings)
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
@NonNull
@@ -209,15 +210,13 @@
* the deprecated header, but it should not be used to identify the webview to first-party
* servers under the control of the app developer.
* <p>
- * The format of the allow-list follows the origin rules of
+ * The format of the strings in the allow-list follows the origin rules of
* {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
*
* @param allowList Set of origins to allow-list.
* @see WebSettingsCompat#setRequestedWithHeaderOriginAllowList(WebSettings, Set)
* @throws IllegalArgumentException if the allow-list contains a malformed origin.
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public abstract void setRequestedWithHeaderOriginAllowList(@NonNull Set<String> allowList);
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 1cb77cb..b612e86 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -695,10 +695,6 @@
}
}
- private static WebSettingsAdapter getAdapter(WebSettings settings) {
- return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings);
- }
-
/**
* Get the currently configured allow-list of origins, which is guaranteed to receive the
* {@code X-Requested-With} HTTP header on requests from the {@link WebView} owning the passed
@@ -706,12 +702,14 @@
* <p>
* Any origin <em>not</em> on this allow-list may not receive the header, depending on the
* current installed WebView provider.
+ * <p>
+ * The format of the strings in the allow-list follows the origin rules of
+ * {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
*
+ * @param settings Settings retrieved from {@link WebView#getSettings()}.
* @return The configured set of allow-listed origins.
* @see #setRequestedWithHeaderOriginAllowList(WebSettings, Set)
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
@NonNull
@@ -738,14 +736,13 @@
* the deprecated header, but it should not be used to identify the webview to first-party
* servers under the control of the app developer.
* <p>
- * The format of the allow-list follows the origin rules of
+ * The format of the strings in the allow-list follows the origin rules of
* {@link WebViewCompat#addWebMessageListener(WebView, String, Set, WebViewCompat.WebMessageListener)}.
*
+ * @param settings Settings retrieved from {@link WebView#getSettings()}.
* @param allowList Set of origins to allow-list.
* @throws IllegalArgumentException if the allow-list contains a malformed origin.
- * @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresFeature(name = WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST,
enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
public static void setRequestedWithHeaderOriginAllowList(@NonNull WebSettings settings,
@@ -758,5 +755,9 @@
throw WebViewFeatureInternal.getUnsupportedOperationException();
}
}
+
+ private static WebSettingsAdapter getAdapter(WebSettings settings) {
+ return WebViewGlueCommunicator.getCompatConverter().convertSettings(settings);
+ }
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java b/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
index 1ef162a..508ff54 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/IncompatibleApkWebViewProviderFactory.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -80,4 +81,10 @@
public ProxyControllerBoundaryInterface getProxyController() {
throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
}
+
+ @NonNull
+ @Override
+ public DropDataContentProviderBoundaryInterface getDropDataProvider() {
+ throw new UnsupportedOperationException(UNSUPPORTED_EXCEPTION_EXPLANATION);
+ }
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
index f406876..6007a34 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
@@ -27,5 +27,5 @@
}
// ProcessGlobalConfig#setDataDirectorySuffix(String)
- public static final String SET_DATA_DIRECTORY_SUFFIX = "SET_DATA_DIRECTORY_SUFFIX";
+ public static final String SET_DATA_DIRECTORY_SUFFIX = "SET_DATA_DIRECTORY_SUFFIX:DEV";
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
index 9eb736b..4c85e7d 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactory.java
@@ -21,6 +21,7 @@
import androidx.annotation.NonNull;
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -80,4 +81,10 @@
*/
@NonNull
ProxyControllerBoundaryInterface getProxyController();
+
+ /**
+ * Fetch the boundary interface representing image drag drop implementation.
+ */
+ @NonNull
+ DropDataContentProviderBoundaryInterface getDropDataProvider();
}
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
index b30a66b..3843c26 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderFactoryAdapter.java
@@ -20,6 +20,7 @@
import androidx.annotation.NonNull;
+import org.chromium.support_lib_boundary.DropDataContentProviderBoundaryInterface;
import org.chromium.support_lib_boundary.ProxyControllerBoundaryInterface;
import org.chromium.support_lib_boundary.ServiceWorkerControllerBoundaryInterface;
import org.chromium.support_lib_boundary.StaticsBoundaryInterface;
@@ -117,4 +118,15 @@
return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
ProxyControllerBoundaryInterface.class, mImpl.getProxyController());
}
+
+ /**
+ * Adapter method for fetching the support library class representing Drag drop
+ * Image implementation.
+ */
+ @NonNull
+ @Override
+ public DropDataContentProviderBoundaryInterface getDropDataProvider() {
+ return BoundaryInterfaceReflectionUtil.castToSuppLibClass(
+ DropDataContentProviderBoundaryInterface.class, mImpl.getDropDataProvider());
+ }
}
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index e3dc726..45ead03 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -82,7 +82,6 @@
}
public final class SplitPairRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
method public boolean getClearTop();
method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
method public int getFinishPrimaryWithSecondary();
@@ -104,7 +103,6 @@
}
public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public int getFinishPrimaryWithPlaceholder();
method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 7c76c1d..00f20da 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -89,7 +89,6 @@
}
public final class SplitPairRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
method public boolean getClearTop();
method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
method public int getFinishPrimaryWithSecondary();
@@ -111,7 +110,6 @@
}
public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public int getFinishPrimaryWithPlaceholder();
method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index e3dc726..45ead03 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -82,7 +82,6 @@
}
public final class SplitPairRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPairRule(java.util.Set<androidx.window.embedding.SplitPairFilter> filters, optional int finishPrimaryWithSecondary, optional int finishSecondaryWithPrimary, optional boolean clearTop, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDir);
method public boolean getClearTop();
method public java.util.Set<androidx.window.embedding.SplitPairFilter> getFilters();
method public int getFinishPrimaryWithSecondary();
@@ -104,7 +103,6 @@
}
public final class SplitPlaceholderRule extends androidx.window.embedding.SplitRule {
- ctor @Deprecated public SplitPlaceholderRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, boolean isSticky, optional int finishPrimaryWithPlaceholder, optional @IntRange(from=0L) int minWidth, optional @IntRange(from=0L) int minSmallestWidth, optional @FloatRange(from=0.0, to=1.0) float splitRatio, optional int layoutDirection);
method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
method public int getFinishPrimaryWithPlaceholder();
method public android.content.Intent getPlaceholderIntent();
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index 54b6ebd..f2b811c 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -26,9 +26,10 @@
import androidx.window.embedding.MatcherUtils.sMatchersTag
/**
- * Filter for [ActivityRule] that checks for component name match. Allows a wildcard symbol in the
- * end or instead of the package name, and a wildcard symbol in the end or instead of the class
- * name.
+ * Filter for [ActivityRule] and [SplitPlaceholderRule] that checks for component name match when
+ * a new activity is started. If the filter matches the started activity [Intent], the activity will
+ * then apply the rule based on the match result. This filter allows a wildcard symbol in the end or
+ * instead of the package name, and a wildcard symbol in the end or instead of the class name.
*/
class ActivityFilter internal constructor(
/**
@@ -89,6 +90,7 @@
) { "Wildcard in class name is only allowed at the end." }
}
+ /** Returns `true` if the [ActivityFilter] matches this [intent]. */
fun matchesIntent(intent: Intent): Boolean {
val match = if (!isIntentMatching(intent, activityComponentInfo)) {
false
@@ -105,6 +107,7 @@
return match
}
+ /** Returns `true` if the [ActivityFilter] matches this [activity]. */
fun matchesActivity(activity: Activity): Boolean {
val match =
isActivityOrIntentMatching(activity, activityComponentInfo) &&
@@ -119,10 +122,14 @@
return match
}
+ /** Returns `true` if the [ActivityFilter] matches the [Class.getName]. */
fun <T : Activity> matchesClassName(clazz: Class<T>): Boolean {
return activityComponentInfo.className == clazz.name
}
+ /**
+ * Returns `true` if the [ActivityFilter] matches the [Class.getName] or includes wildcard (`*`)
+ */
fun <T : Activity> matchesClassNameOrWildCard(clazz: Class<T>?): Boolean {
return clazz?.let(::matchesClassName) ?: activityComponentInfo.className.contains("*")
}
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index 95e1625..daf4a46 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -26,8 +26,10 @@
import androidx.window.embedding.MatcherUtils.sMatchersTag
/**
- * Filter used to find if a pair of activities should be put in a split. Applied to the base /
- * primary activity and an intent starting a secondary activity.
+ * Filter for [SplitPairRule] and used to find if a pair of activities should be put in a split.
+ * It is used when a new activity is started from the primary activity.
+ * If the filter matches the primary [Activity.getComponentName] and the new started activity
+ * [Intent], it matches the [SplitPairRule] that holds this filter.
*/
class SplitPairFilter(
/**
@@ -59,6 +61,9 @@
private val secondaryActivityInfo: ActivityComponentInfo
get() = ActivityComponentInfo(secondaryActivityName)
+ /**
+ * Returns `true` if this [SplitPairFilter] matches [primaryActivity] and [secondaryActivity].
+ */
fun matchesActivityPair(primaryActivity: Activity, secondaryActivity: Activity): Boolean {
// Check if the activity component names match
var match = areComponentsMatching(primaryActivity.componentName, primaryActivityName) &&
@@ -79,6 +84,10 @@
return match
}
+ /**
+ * Returns `true` if this [SplitPairFilter] matches the [primaryActivity] and the
+ * [secondaryActivityIntent]
+ */
fun matchesActivityIntentPair(
primaryActivity: Activity,
secondaryActivityIntent: Intent
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
index de62416..24c497f 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -61,12 +61,7 @@
*/
val clearTop: Boolean
- // TODO(b/229656253): Reduce visibility to remove from public API.
- @Deprecated(
- message = "Visibility of the constructor will be reduced.",
- replaceWith = ReplaceWith("androidx.window.embedding.SplitPairRule.Builder")
- )
- constructor(
+ internal constructor(
filters: Set<SplitPairFilter>,
@SplitFinishBehavior finishPrimaryWithSecondary: Int = FINISH_NEVER,
@SplitFinishBehavior finishSecondaryWithPrimary: Int = FINISH_ALWAYS,
@@ -144,7 +139,6 @@
fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
apply { this.layoutDir = layoutDir }
- @Suppress("DEPRECATION")
fun build() = SplitPairRule(filters, finishPrimaryWithSecondary, finishSecondaryWithPrimary,
clearTop, minWidth, minSmallestWidth, splitRatio, layoutDir)
}
@@ -157,17 +151,13 @@
val newSet = mutableSetOf<SplitPairFilter>()
newSet.addAll(filters)
newSet.add(filter)
- @Suppress("DEPRECATION")
- return SplitPairRule(
- newSet.toSet(),
- finishPrimaryWithSecondary,
- finishSecondaryWithPrimary,
- clearTop,
- minWidth,
- minSmallestWidth,
- splitRatio,
- layoutDirection
- )
+ return Builder(newSet.toSet(), minWidth, minSmallestWidth)
+ .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
+ .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
+ .setClearTop(clearTop)
+ .setSplitRatio(splitRatio)
+ .setLayoutDir(layoutDirection)
+ .build()
}
override fun equals(other: Any?): Boolean {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
index ae4b432..5c2c6ee 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -63,12 +63,7 @@
@SplitPlaceholderFinishBehavior
val finishPrimaryWithPlaceholder: Int
- // TODO(b/229656253): Reduce visibility to remove from public API.
- @Deprecated(
- message = "Visibility of the constructor will be reduced.",
- replaceWith = ReplaceWith("androidx.window.embedding.SplitPlaceholderRule.Builder")
- )
- constructor(
+ internal constructor(
filters: Set<ActivityFilter>,
placeholderIntent: Intent,
isSticky: Boolean,
@@ -142,7 +137,6 @@
fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
apply { this.layoutDir = layoutDir }
- @Suppress("DEPRECATION")
fun build() = SplitPlaceholderRule(filters, placeholderIntent, isSticky,
finishPrimaryWithPlaceholder, minWidth, minSmallestWidth, splitRatio, layoutDir)
}
@@ -155,17 +149,12 @@
val newSet = mutableSetOf<ActivityFilter>()
newSet.addAll(filters)
newSet.add(filter)
- @Suppress("DEPRECATION")
- return SplitPlaceholderRule(
- newSet.toSet(),
- placeholderIntent,
- isSticky,
- finishPrimaryWithPlaceholder,
- minWidth,
- minSmallestWidth,
- splitRatio,
- layoutDirection
- )
+ return Builder(newSet.toSet(), placeholderIntent, minWidth, minSmallestWidth)
+ .setSticky(isSticky)
+ .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
+ .setSplitRatio(splitRatio)
+ .setLayoutDir(layoutDirection)
+ .build()
}
override fun equals(other: Any?): Boolean {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
index b9b8ea1..5cfea09 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
@@ -156,17 +156,13 @@
clearTop =
getBoolean(R.styleable.SplitPairRule_clearTop, false)
}
- @Suppress("DEPRECATION")
- return SplitPairRule(
- emptySet(),
- finishPrimaryWithSecondary,
- finishSecondaryWithPrimary,
- clearTop,
- minWidth,
- minSmallestWidth,
- ratio,
- layoutDir
- )
+ return SplitPairRule.Builder(emptySet(), minWidth, minSmallestWidth)
+ .setFinishPrimaryWithSecondary(finishPrimaryWithSecondary)
+ .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
+ .setClearTop(clearTop)
+ .setSplitRatio(ratio)
+ .setLayoutDir(layoutDir)
+ .build()
}
private fun parseSplitPlaceholderRule(
@@ -219,17 +215,16 @@
placeholderActivityIntentName
)
- @Suppress("DEPRECATION")
- return SplitPlaceholderRule(
+ return SplitPlaceholderRule.Builder(
emptySet(),
Intent().setComponent(placeholderActivityClassName),
- stickyPlaceholder,
- finishPrimaryWithPlaceholder,
minWidth,
- minSmallestWidth,
- ratio,
- layoutDir
- )
+ minSmallestWidth
+ ).setSticky(stickyPlaceholder)
+ .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
+ .setSplitRatio(ratio)
+ .setLayoutDir(layoutDir)
+ .build()
}
private fun parseSplitPairFilter(