Merge "Split out Flow.Spotting into its own record type." 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/api/current.txt b/health/connect/connect-client/api/current.txt
index 037d48a..081ecbd 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -138,13 +138,13 @@
}
public final class BasalBodyTemperatureRecord implements androidx.health.connect.client.records.Record {
- ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -169,53 +169,69 @@
}
public final class BloodGlucoseRecord implements androidx.health.connect.client.records.Record {
- ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional String? specimenSource, optional String? mealType, optional String? relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional int specimenSource, optional int mealType, optional int relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.BloodGlucose getLevel();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getRelationToMeal();
- method public String? getSpecimenSource();
+ method public int getRelationToMeal();
+ method public int getSpecimenSource();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public final androidx.health.connect.client.units.BloodGlucose level;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? relationToMeal;
- property public final String? specimenSource;
+ property public final int relationToMeal;
+ property public final int specimenSource;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.BloodGlucoseRecord.Companion Companion;
+ field public static final int RELATION_TO_MEAL_AFTER_MEAL = 4; // 0x4
+ field public static final int RELATION_TO_MEAL_BEFORE_MEAL = 3; // 0x3
+ field public static final int RELATION_TO_MEAL_FASTING = 2; // 0x2
+ field public static final int RELATION_TO_MEAL_GENERAL = 1; // 0x1
+ field public static final int RELATION_TO_MEAL_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_CAPILLARY_BLOOD = 2; // 0x2
+ field public static final int SPECIMEN_SOURCE_INTERSTITIAL_FLUID = 1; // 0x1
+ field public static final int SPECIMEN_SOURCE_PLASMA = 3; // 0x3
+ field public static final int SPECIMEN_SOURCE_SERUM = 4; // 0x4
+ field public static final int SPECIMEN_SOURCE_TEARS = 5; // 0x5
+ field public static final int SPECIMEN_SOURCE_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_WHOLE_BLOOD = 6; // 0x6
}
- public static final class BloodGlucoseRecord.SpecimenSource {
- field public static final String CAPILLARY_BLOOD = "capillary_blood";
- field public static final androidx.health.connect.client.records.BloodGlucoseRecord.SpecimenSource INSTANCE;
- field public static final String INTERSTITIAL_FLUID = "interstitial_fluid";
- field public static final String PLASMA = "plasma";
- field public static final String SERUM = "serum";
- field public static final String TEARS = "tears";
- field public static final String WHOLE_BLOOD = "whole_blood";
+ public static final class BloodGlucoseRecord.Companion {
}
public final class BloodPressureRecord implements androidx.health.connect.client.records.Record {
- ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getBodyPosition();
+ ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional int bodyPosition, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getBodyPosition();
method public androidx.health.connect.client.units.Pressure getDiastolic();
- method public String? getMeasurementLocation();
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Pressure getSystolic();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? bodyPosition;
+ property public final int bodyPosition;
property public final androidx.health.connect.client.units.Pressure diastolic;
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Pressure systolic;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int BODY_POSITION_LYING_DOWN = 3; // 0x3
+ field public static final int BODY_POSITION_RECLINING = 4; // 0x4
+ field public static final int BODY_POSITION_SITTING_DOWN = 2; // 0x2
+ field public static final int BODY_POSITION_STANDING_UP = 1; // 0x1
+ field public static final int BODY_POSITION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+ field public static final int MEASUREMENT_LOCATION_LEFT_UPPER_ARM = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_LEFT_WRIST = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_RIGHT_UPPER_ARM = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RIGHT_WRIST = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
@@ -224,14 +240,6 @@
public static final class BloodPressureRecord.Companion {
}
- public static final class BloodPressureRecord.MeasurementLocation {
- field public static final androidx.health.connect.client.records.BloodPressureRecord.MeasurementLocation INSTANCE;
- field public static final String LEFT_UPPER_ARM = "left_upper_arm";
- field public static final String LEFT_WRIST = "left_wrist";
- field public static final String RIGHT_UPPER_ARM = "right_upper_arm";
- field public static final String RIGHT_WRIST = "right_wrist";
- }
-
public final class BodyFatRecord implements androidx.health.connect.client.records.Record {
ctor public BodyFatRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Percentage percentage, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -244,36 +252,29 @@
property public java.time.ZoneOffset? zoneOffset;
}
- public final class BodyPosition {
- field public static final androidx.health.connect.client.records.BodyPosition INSTANCE;
- field public static final String LYING_DOWN = "lying_down";
- field public static final String RECLINING = "reclining";
- field public static final String SITTING_DOWN = "sitting_down";
- field public static final String STANDING_UP = "standing_up";
- }
-
public final class BodyTemperatureMeasurementLocation {
- field public static final String ARMPIT = "armpit";
- field public static final String EAR = "ear";
- field public static final String FINGER = "finger";
- field public static final String FOREHEAD = "forehead";
field public static final androidx.health.connect.client.records.BodyTemperatureMeasurementLocation INSTANCE;
- field public static final String MOUTH = "mouth";
- field public static final String RECTUM = "rectum";
- field public static final String TEMPORAL_ARTERY = "temporal_artery";
- field public static final String TOE = "toe";
- field public static final String VAGINA = "vagina";
- field public static final String WRIST = "wrist";
+ field public static final int MEASUREMENT_LOCATION_ARMPIT = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_EAR = 8; // 0x8
+ field public static final int MEASUREMENT_LOCATION_FINGER = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_FOREHEAD = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_MOUTH = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RECTUM = 5; // 0x5
+ field public static final int MEASUREMENT_LOCATION_TEMPORAL_ARTERY = 6; // 0x6
+ field public static final int MEASUREMENT_LOCATION_TOE = 7; // 0x7
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
+ field public static final int MEASUREMENT_LOCATION_VAGINA = 10; // 0xa
+ field public static final int MEASUREMENT_LOCATION_WRIST = 9; // 0x9
}
public final class BodyTemperatureRecord implements androidx.health.connect.client.records.Record {
- ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -395,25 +396,26 @@
}
public final class ExerciseEventRecord implements androidx.health.connect.client.records.Record {
- ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
- method public String getEventType();
+ method public int getEventType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
- property public final String eventType;
+ property public final int eventType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
+ field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
+ field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
+ field public static final int EVENT_TYPE_REST = 2; // 0x2
+ field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
}
- public static final class ExerciseEventRecord.EventType {
- field public static final androidx.health.connect.client.records.ExerciseEventRecord.EventType INSTANCE;
- field public static final String PAUSE = "pause";
- field public static final String REST = "rest";
+ public static final class ExerciseEventRecord.Companion {
}
public final class ExerciseLapRecord implements androidx.health.connect.client.records.Record {
@@ -711,12 +713,12 @@
}
public final class MealType {
- field public static final String BREAKFAST = "breakfast";
- field public static final String DINNER = "dinner";
field public static final androidx.health.connect.client.records.MealType INSTANCE;
- field public static final String LUNCH = "lunch";
- field public static final String SNACK = "snack";
- field public static final String UNKNOWN = "unknown";
+ field public static final int MEAL_TYPE_BREAKFAST = 1; // 0x1
+ field public static final int MEAL_TYPE_DINNER = 3; // 0x3
+ field public static final int MEAL_TYPE_LUNCH = 2; // 0x2
+ field public static final int MEAL_TYPE_SNACK = 4; // 0x4
+ field public static final int MEAL_TYPE_UNKNOWN = 0; // 0x0
}
public final class MenstruationFlowRecord implements androidx.health.connect.client.records.Record {
@@ -740,7 +742,7 @@
}
public final class NutritionRecord implements androidx.health.connect.client.records.Record {
- ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional int mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass? getBiotin();
method public androidx.health.connect.client.units.Mass? getCaffeine();
method public androidx.health.connect.client.units.Mass? getCalcium();
@@ -759,7 +761,7 @@
method public androidx.health.connect.client.units.Mass? getIron();
method public androidx.health.connect.client.units.Mass? getMagnesium();
method public androidx.health.connect.client.units.Mass? getManganese();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Mass? getMolybdenum();
method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
@@ -808,7 +810,7 @@
property public final androidx.health.connect.client.units.Mass? iron;
property public final androidx.health.connect.client.units.Mass? magnesium;
property public final androidx.health.connect.client.units.Mass? manganese;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Mass? molybdenum;
property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
@@ -888,23 +890,23 @@
}
public final class OvulationTestRecord implements androidx.health.connect.client.records.Record {
- ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, String result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, int result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String getResult();
+ method public int getResult();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String result;
+ property public final int result;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.OvulationTestRecord.Companion Companion;
+ field public static final int RESULT_HIGH = 2; // 0x2
+ field public static final int RESULT_INCONCLUSIVE = 0; // 0x0
+ field public static final int RESULT_NEGATIVE = 3; // 0x3
+ field public static final int RESULT_POSITIVE = 1; // 0x1
}
- public static final class OvulationTestRecord.Result {
- field public static final String HIGH = "high";
- field public static final String INCONCLUSIVE = "inconclusive";
- field public static final androidx.health.connect.client.records.OvulationTestRecord.Result INSTANCE;
- field public static final String NEGATIVE = "negative";
- field public static final String POSITIVE = "positive";
+ public static final class OvulationTestRecord.Companion {
}
public final class OxygenSaturationRecord implements androidx.health.connect.client.records.Record {
@@ -955,14 +957,6 @@
property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
}
- public final class RelationToMeal {
- field public static final String AFTER_MEAL = "after_meal";
- field public static final String BEFORE_MEAL = "before_meal";
- field public static final String FASTING = "fasting";
- field public static final String GENERAL = "general";
- field public static final androidx.health.connect.client.records.RelationToMeal INSTANCE;
- }
-
public final class RespiratoryRateRecord implements androidx.health.connect.client.records.Record {
ctor public RespiratoryRateRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double rate, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -995,21 +989,22 @@
}
public final class SexualActivityRecord implements androidx.health.connect.client.records.Record {
- ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getProtectionUsed();
+ method public int getProtectionUsed();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? protectionUsed;
+ property public final int protectionUsed;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.SexualActivityRecord.Companion Companion;
+ field public static final int PROTECTION_USED_PROTECTED = 1; // 0x1
+ field public static final int PROTECTION_USED_UNKNOWN = 0; // 0x0
+ field public static final int PROTECTION_USED_UNPROTECTED = 2; // 0x2
}
- public static final class SexualActivityRecord.Protection {
- field public static final androidx.health.connect.client.records.SexualActivityRecord.Protection INSTANCE;
- field public static final String PROTECTED = "protected";
- field public static final String UNPROTECTED = "unprotected";
+ public static final class SexualActivityRecord.Companion {
}
public final class SleepSessionRecord implements androidx.health.connect.client.records.Record {
@@ -1146,31 +1141,31 @@
}
public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.Record {
- ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public long getCount();
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
- method public String getType();
+ method public int getType();
property public final long count;
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
- property public final String type;
+ property public final int type;
+ field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
+ field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
+ field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
+ field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
+ field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
+ field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
+ field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
}
- public static final class SwimmingStrokesRecord.SwimmingType {
- field public static final String BACKSTROKE = "backstroke";
- field public static final String BREASTSTROKE = "breaststroke";
- field public static final String BUTTERFLY = "butterfly";
- field public static final String FREESTYLE = "freestyle";
- field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.SwimmingType INSTANCE;
- field public static final String MIXED = "mixed";
- field public static final String OTHER = "other";
+ public static final class SwimmingStrokesRecord.Companion {
}
public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
@@ -1195,27 +1190,27 @@
}
public final class Vo2MaxRecord implements androidx.health.connect.client.records.Record {
- ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional String? measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementMethod();
+ ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional int measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementMethod();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getTime();
method public double getVo2MillilitersPerMinuteKilogram();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementMethod;
+ property public final int measurementMethod;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant time;
property public final double vo2MillilitersPerMinuteKilogram;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.Vo2MaxRecord.Companion Companion;
+ field public static final int MEASUREMENT_METHOD_COOPER_TEST = 3; // 0x3
+ field public static final int MEASUREMENT_METHOD_HEART_RATE_RATIO = 2; // 0x2
+ field public static final int MEASUREMENT_METHOD_METABOLIC_CART = 1; // 0x1
+ field public static final int MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST = 4; // 0x4
+ field public static final int MEASUREMENT_METHOD_OTHER = 0; // 0x0
+ field public static final int MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST = 5; // 0x5
}
- public static final class Vo2MaxRecord.MeasurementMethod {
- field public static final String COOPER_TEST = "cooper_test";
- field public static final String HEART_RATE_RATIO = "heart_rate_ratio";
- field public static final androidx.health.connect.client.records.Vo2MaxRecord.MeasurementMethod INSTANCE;
- field public static final String METABOLIC_CART = "metabolic_cart";
- field public static final String MULTISTAGE_FITNESS_TEST = "multistage_fitness_test";
- field public static final String OTHER = "other";
- field public static final String ROCKPORT_FITNESS_TEST = "rockport_fitness_test";
+ public static final class Vo2MaxRecord.Companion {
}
public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 037d48a..081ecbd 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -138,13 +138,13 @@
}
public final class BasalBodyTemperatureRecord implements androidx.health.connect.client.records.Record {
- ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -169,53 +169,69 @@
}
public final class BloodGlucoseRecord implements androidx.health.connect.client.records.Record {
- ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional String? specimenSource, optional String? mealType, optional String? relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional int specimenSource, optional int mealType, optional int relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.BloodGlucose getLevel();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getRelationToMeal();
- method public String? getSpecimenSource();
+ method public int getRelationToMeal();
+ method public int getSpecimenSource();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public final androidx.health.connect.client.units.BloodGlucose level;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? relationToMeal;
- property public final String? specimenSource;
+ property public final int relationToMeal;
+ property public final int specimenSource;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.BloodGlucoseRecord.Companion Companion;
+ field public static final int RELATION_TO_MEAL_AFTER_MEAL = 4; // 0x4
+ field public static final int RELATION_TO_MEAL_BEFORE_MEAL = 3; // 0x3
+ field public static final int RELATION_TO_MEAL_FASTING = 2; // 0x2
+ field public static final int RELATION_TO_MEAL_GENERAL = 1; // 0x1
+ field public static final int RELATION_TO_MEAL_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_CAPILLARY_BLOOD = 2; // 0x2
+ field public static final int SPECIMEN_SOURCE_INTERSTITIAL_FLUID = 1; // 0x1
+ field public static final int SPECIMEN_SOURCE_PLASMA = 3; // 0x3
+ field public static final int SPECIMEN_SOURCE_SERUM = 4; // 0x4
+ field public static final int SPECIMEN_SOURCE_TEARS = 5; // 0x5
+ field public static final int SPECIMEN_SOURCE_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_WHOLE_BLOOD = 6; // 0x6
}
- public static final class BloodGlucoseRecord.SpecimenSource {
- field public static final String CAPILLARY_BLOOD = "capillary_blood";
- field public static final androidx.health.connect.client.records.BloodGlucoseRecord.SpecimenSource INSTANCE;
- field public static final String INTERSTITIAL_FLUID = "interstitial_fluid";
- field public static final String PLASMA = "plasma";
- field public static final String SERUM = "serum";
- field public static final String TEARS = "tears";
- field public static final String WHOLE_BLOOD = "whole_blood";
+ public static final class BloodGlucoseRecord.Companion {
}
public final class BloodPressureRecord implements androidx.health.connect.client.records.Record {
- ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getBodyPosition();
+ ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional int bodyPosition, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getBodyPosition();
method public androidx.health.connect.client.units.Pressure getDiastolic();
- method public String? getMeasurementLocation();
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Pressure getSystolic();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? bodyPosition;
+ property public final int bodyPosition;
property public final androidx.health.connect.client.units.Pressure diastolic;
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Pressure systolic;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int BODY_POSITION_LYING_DOWN = 3; // 0x3
+ field public static final int BODY_POSITION_RECLINING = 4; // 0x4
+ field public static final int BODY_POSITION_SITTING_DOWN = 2; // 0x2
+ field public static final int BODY_POSITION_STANDING_UP = 1; // 0x1
+ field public static final int BODY_POSITION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+ field public static final int MEASUREMENT_LOCATION_LEFT_UPPER_ARM = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_LEFT_WRIST = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_RIGHT_UPPER_ARM = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RIGHT_WRIST = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
@@ -224,14 +240,6 @@
public static final class BloodPressureRecord.Companion {
}
- public static final class BloodPressureRecord.MeasurementLocation {
- field public static final androidx.health.connect.client.records.BloodPressureRecord.MeasurementLocation INSTANCE;
- field public static final String LEFT_UPPER_ARM = "left_upper_arm";
- field public static final String LEFT_WRIST = "left_wrist";
- field public static final String RIGHT_UPPER_ARM = "right_upper_arm";
- field public static final String RIGHT_WRIST = "right_wrist";
- }
-
public final class BodyFatRecord implements androidx.health.connect.client.records.Record {
ctor public BodyFatRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Percentage percentage, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -244,36 +252,29 @@
property public java.time.ZoneOffset? zoneOffset;
}
- public final class BodyPosition {
- field public static final androidx.health.connect.client.records.BodyPosition INSTANCE;
- field public static final String LYING_DOWN = "lying_down";
- field public static final String RECLINING = "reclining";
- field public static final String SITTING_DOWN = "sitting_down";
- field public static final String STANDING_UP = "standing_up";
- }
-
public final class BodyTemperatureMeasurementLocation {
- field public static final String ARMPIT = "armpit";
- field public static final String EAR = "ear";
- field public static final String FINGER = "finger";
- field public static final String FOREHEAD = "forehead";
field public static final androidx.health.connect.client.records.BodyTemperatureMeasurementLocation INSTANCE;
- field public static final String MOUTH = "mouth";
- field public static final String RECTUM = "rectum";
- field public static final String TEMPORAL_ARTERY = "temporal_artery";
- field public static final String TOE = "toe";
- field public static final String VAGINA = "vagina";
- field public static final String WRIST = "wrist";
+ field public static final int MEASUREMENT_LOCATION_ARMPIT = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_EAR = 8; // 0x8
+ field public static final int MEASUREMENT_LOCATION_FINGER = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_FOREHEAD = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_MOUTH = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RECTUM = 5; // 0x5
+ field public static final int MEASUREMENT_LOCATION_TEMPORAL_ARTERY = 6; // 0x6
+ field public static final int MEASUREMENT_LOCATION_TOE = 7; // 0x7
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
+ field public static final int MEASUREMENT_LOCATION_VAGINA = 10; // 0xa
+ field public static final int MEASUREMENT_LOCATION_WRIST = 9; // 0x9
}
public final class BodyTemperatureRecord implements androidx.health.connect.client.records.Record {
- ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -395,25 +396,26 @@
}
public final class ExerciseEventRecord implements androidx.health.connect.client.records.Record {
- ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
- method public String getEventType();
+ method public int getEventType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
- property public final String eventType;
+ property public final int eventType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
+ field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
+ field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
+ field public static final int EVENT_TYPE_REST = 2; // 0x2
+ field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
}
- public static final class ExerciseEventRecord.EventType {
- field public static final androidx.health.connect.client.records.ExerciseEventRecord.EventType INSTANCE;
- field public static final String PAUSE = "pause";
- field public static final String REST = "rest";
+ public static final class ExerciseEventRecord.Companion {
}
public final class ExerciseLapRecord implements androidx.health.connect.client.records.Record {
@@ -711,12 +713,12 @@
}
public final class MealType {
- field public static final String BREAKFAST = "breakfast";
- field public static final String DINNER = "dinner";
field public static final androidx.health.connect.client.records.MealType INSTANCE;
- field public static final String LUNCH = "lunch";
- field public static final String SNACK = "snack";
- field public static final String UNKNOWN = "unknown";
+ field public static final int MEAL_TYPE_BREAKFAST = 1; // 0x1
+ field public static final int MEAL_TYPE_DINNER = 3; // 0x3
+ field public static final int MEAL_TYPE_LUNCH = 2; // 0x2
+ field public static final int MEAL_TYPE_SNACK = 4; // 0x4
+ field public static final int MEAL_TYPE_UNKNOWN = 0; // 0x0
}
public final class MenstruationFlowRecord implements androidx.health.connect.client.records.Record {
@@ -740,7 +742,7 @@
}
public final class NutritionRecord implements androidx.health.connect.client.records.Record {
- ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional int mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass? getBiotin();
method public androidx.health.connect.client.units.Mass? getCaffeine();
method public androidx.health.connect.client.units.Mass? getCalcium();
@@ -759,7 +761,7 @@
method public androidx.health.connect.client.units.Mass? getIron();
method public androidx.health.connect.client.units.Mass? getMagnesium();
method public androidx.health.connect.client.units.Mass? getManganese();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Mass? getMolybdenum();
method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
@@ -808,7 +810,7 @@
property public final androidx.health.connect.client.units.Mass? iron;
property public final androidx.health.connect.client.units.Mass? magnesium;
property public final androidx.health.connect.client.units.Mass? manganese;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Mass? molybdenum;
property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
@@ -888,23 +890,23 @@
}
public final class OvulationTestRecord implements androidx.health.connect.client.records.Record {
- ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, String result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, int result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String getResult();
+ method public int getResult();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String result;
+ property public final int result;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.OvulationTestRecord.Companion Companion;
+ field public static final int RESULT_HIGH = 2; // 0x2
+ field public static final int RESULT_INCONCLUSIVE = 0; // 0x0
+ field public static final int RESULT_NEGATIVE = 3; // 0x3
+ field public static final int RESULT_POSITIVE = 1; // 0x1
}
- public static final class OvulationTestRecord.Result {
- field public static final String HIGH = "high";
- field public static final String INCONCLUSIVE = "inconclusive";
- field public static final androidx.health.connect.client.records.OvulationTestRecord.Result INSTANCE;
- field public static final String NEGATIVE = "negative";
- field public static final String POSITIVE = "positive";
+ public static final class OvulationTestRecord.Companion {
}
public final class OxygenSaturationRecord implements androidx.health.connect.client.records.Record {
@@ -955,14 +957,6 @@
property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
}
- public final class RelationToMeal {
- field public static final String AFTER_MEAL = "after_meal";
- field public static final String BEFORE_MEAL = "before_meal";
- field public static final String FASTING = "fasting";
- field public static final String GENERAL = "general";
- field public static final androidx.health.connect.client.records.RelationToMeal INSTANCE;
- }
-
public final class RespiratoryRateRecord implements androidx.health.connect.client.records.Record {
ctor public RespiratoryRateRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double rate, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -995,21 +989,22 @@
}
public final class SexualActivityRecord implements androidx.health.connect.client.records.Record {
- ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getProtectionUsed();
+ method public int getProtectionUsed();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? protectionUsed;
+ property public final int protectionUsed;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.SexualActivityRecord.Companion Companion;
+ field public static final int PROTECTION_USED_PROTECTED = 1; // 0x1
+ field public static final int PROTECTION_USED_UNKNOWN = 0; // 0x0
+ field public static final int PROTECTION_USED_UNPROTECTED = 2; // 0x2
}
- public static final class SexualActivityRecord.Protection {
- field public static final androidx.health.connect.client.records.SexualActivityRecord.Protection INSTANCE;
- field public static final String PROTECTED = "protected";
- field public static final String UNPROTECTED = "unprotected";
+ public static final class SexualActivityRecord.Companion {
}
public final class SleepSessionRecord implements androidx.health.connect.client.records.Record {
@@ -1146,31 +1141,31 @@
}
public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.Record {
- ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public long getCount();
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
- method public String getType();
+ method public int getType();
property public final long count;
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
- property public final String type;
+ property public final int type;
+ field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
+ field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
+ field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
+ field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
+ field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
+ field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
+ field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
}
- public static final class SwimmingStrokesRecord.SwimmingType {
- field public static final String BACKSTROKE = "backstroke";
- field public static final String BREASTSTROKE = "breaststroke";
- field public static final String BUTTERFLY = "butterfly";
- field public static final String FREESTYLE = "freestyle";
- field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.SwimmingType INSTANCE;
- field public static final String MIXED = "mixed";
- field public static final String OTHER = "other";
+ public static final class SwimmingStrokesRecord.Companion {
}
public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.Record {
@@ -1195,27 +1190,27 @@
}
public final class Vo2MaxRecord implements androidx.health.connect.client.records.Record {
- ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional String? measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementMethod();
+ ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional int measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementMethod();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getTime();
method public double getVo2MillilitersPerMinuteKilogram();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementMethod;
+ property public final int measurementMethod;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant time;
property public final double vo2MillilitersPerMinuteKilogram;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.Vo2MaxRecord.Companion Companion;
+ field public static final int MEASUREMENT_METHOD_COOPER_TEST = 3; // 0x3
+ field public static final int MEASUREMENT_METHOD_HEART_RATE_RATIO = 2; // 0x2
+ field public static final int MEASUREMENT_METHOD_METABOLIC_CART = 1; // 0x1
+ field public static final int MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST = 4; // 0x4
+ field public static final int MEASUREMENT_METHOD_OTHER = 0; // 0x0
+ field public static final int MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST = 5; // 0x5
}
- public static final class Vo2MaxRecord.MeasurementMethod {
- field public static final String COOPER_TEST = "cooper_test";
- field public static final String HEART_RATE_RATIO = "heart_rate_ratio";
- field public static final androidx.health.connect.client.records.Vo2MaxRecord.MeasurementMethod INSTANCE;
- field public static final String METABOLIC_CART = "metabolic_cart";
- field public static final String MULTISTAGE_FITNESS_TEST = "multistage_fitness_test";
- field public static final String OTHER = "other";
- field public static final String ROCKPORT_FITNESS_TEST = "rockport_fitness_test";
+ public static final class Vo2MaxRecord.Companion {
}
public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 8cfec2e..a9c1a0f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -138,13 +138,13 @@
}
public final class BasalBodyTemperatureRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BasalBodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -169,53 +169,69 @@
}
public final class BloodGlucoseRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional String? specimenSource, optional String? mealType, optional String? relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public BloodGlucoseRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.BloodGlucose level, optional int specimenSource, optional int mealType, optional int relationToMeal, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.BloodGlucose getLevel();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getRelationToMeal();
- method public String? getSpecimenSource();
+ method public int getRelationToMeal();
+ method public int getSpecimenSource();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public final androidx.health.connect.client.units.BloodGlucose level;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? relationToMeal;
- property public final String? specimenSource;
+ property public final int relationToMeal;
+ property public final int specimenSource;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.BloodGlucoseRecord.Companion Companion;
+ field public static final int RELATION_TO_MEAL_AFTER_MEAL = 4; // 0x4
+ field public static final int RELATION_TO_MEAL_BEFORE_MEAL = 3; // 0x3
+ field public static final int RELATION_TO_MEAL_FASTING = 2; // 0x2
+ field public static final int RELATION_TO_MEAL_GENERAL = 1; // 0x1
+ field public static final int RELATION_TO_MEAL_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_CAPILLARY_BLOOD = 2; // 0x2
+ field public static final int SPECIMEN_SOURCE_INTERSTITIAL_FLUID = 1; // 0x1
+ field public static final int SPECIMEN_SOURCE_PLASMA = 3; // 0x3
+ field public static final int SPECIMEN_SOURCE_SERUM = 4; // 0x4
+ field public static final int SPECIMEN_SOURCE_TEARS = 5; // 0x5
+ field public static final int SPECIMEN_SOURCE_UNKNOWN = 0; // 0x0
+ field public static final int SPECIMEN_SOURCE_WHOLE_BLOOD = 6; // 0x6
}
- public static final class BloodGlucoseRecord.SpecimenSource {
- field public static final String CAPILLARY_BLOOD = "capillary_blood";
- field public static final androidx.health.connect.client.records.BloodGlucoseRecord.SpecimenSource INSTANCE;
- field public static final String INTERSTITIAL_FLUID = "interstitial_fluid";
- field public static final String PLASMA = "plasma";
- field public static final String SERUM = "serum";
- field public static final String TEARS = "tears";
- field public static final String WHOLE_BLOOD = "whole_blood";
+ public static final class BloodGlucoseRecord.Companion {
}
public final class BloodPressureRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional String? bodyPosition, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getBodyPosition();
+ ctor public BloodPressureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Pressure systolic, androidx.health.connect.client.units.Pressure diastolic, optional int bodyPosition, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getBodyPosition();
method public androidx.health.connect.client.units.Pressure getDiastolic();
- method public String? getMeasurementLocation();
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Pressure getSystolic();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? bodyPosition;
+ property public final int bodyPosition;
property public final androidx.health.connect.client.units.Pressure diastolic;
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Pressure systolic;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final int BODY_POSITION_LYING_DOWN = 3; // 0x3
+ field public static final int BODY_POSITION_RECLINING = 4; // 0x4
+ field public static final int BODY_POSITION_SITTING_DOWN = 2; // 0x2
+ field public static final int BODY_POSITION_STANDING_UP = 1; // 0x1
+ field public static final int BODY_POSITION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.records.BloodPressureRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> DIASTOLIC_MIN;
+ field public static final int MEASUREMENT_LOCATION_LEFT_UPPER_ARM = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_LEFT_WRIST = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_RIGHT_UPPER_ARM = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RIGHT_WRIST = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_AVG;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MAX;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<androidx.health.connect.client.units.Pressure> SYSTOLIC_MIN;
@@ -224,14 +240,6 @@
public static final class BloodPressureRecord.Companion {
}
- public static final class BloodPressureRecord.MeasurementLocation {
- field public static final androidx.health.connect.client.records.BloodPressureRecord.MeasurementLocation INSTANCE;
- field public static final String LEFT_UPPER_ARM = "left_upper_arm";
- field public static final String LEFT_WRIST = "left_wrist";
- field public static final String RIGHT_UPPER_ARM = "right_upper_arm";
- field public static final String RIGHT_WRIST = "right_wrist";
- }
-
public final class BodyFatRecord implements androidx.health.connect.client.records.InstantaneousRecord {
ctor public BodyFatRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Percentage percentage, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -244,36 +252,29 @@
property public java.time.ZoneOffset? zoneOffset;
}
- public final class BodyPosition {
- field public static final androidx.health.connect.client.records.BodyPosition INSTANCE;
- field public static final String LYING_DOWN = "lying_down";
- field public static final String RECLINING = "reclining";
- field public static final String SITTING_DOWN = "sitting_down";
- field public static final String STANDING_UP = "standing_up";
- }
-
public final class BodyTemperatureMeasurementLocation {
- field public static final String ARMPIT = "armpit";
- field public static final String EAR = "ear";
- field public static final String FINGER = "finger";
- field public static final String FOREHEAD = "forehead";
field public static final androidx.health.connect.client.records.BodyTemperatureMeasurementLocation INSTANCE;
- field public static final String MOUTH = "mouth";
- field public static final String RECTUM = "rectum";
- field public static final String TEMPORAL_ARTERY = "temporal_artery";
- field public static final String TOE = "toe";
- field public static final String VAGINA = "vagina";
- field public static final String WRIST = "wrist";
+ field public static final int MEASUREMENT_LOCATION_ARMPIT = 1; // 0x1
+ field public static final int MEASUREMENT_LOCATION_EAR = 8; // 0x8
+ field public static final int MEASUREMENT_LOCATION_FINGER = 2; // 0x2
+ field public static final int MEASUREMENT_LOCATION_FOREHEAD = 3; // 0x3
+ field public static final int MEASUREMENT_LOCATION_MOUTH = 4; // 0x4
+ field public static final int MEASUREMENT_LOCATION_RECTUM = 5; // 0x5
+ field public static final int MEASUREMENT_LOCATION_TEMPORAL_ARTERY = 6; // 0x6
+ field public static final int MEASUREMENT_LOCATION_TOE = 7; // 0x7
+ field public static final int MEASUREMENT_LOCATION_UNKNOWN = 0; // 0x0
+ field public static final int MEASUREMENT_LOCATION_VAGINA = 10; // 0xa
+ field public static final int MEASUREMENT_LOCATION_WRIST = 9; // 0x9
}
public final class BodyTemperatureRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional String? measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementLocation();
+ ctor public BodyTemperatureRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, androidx.health.connect.client.units.Temperature temperature, optional int measurementLocation, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementLocation();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Temperature getTemperature();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementLocation;
+ property public final int measurementLocation;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Temperature temperature;
property public java.time.Instant time;
@@ -395,25 +396,26 @@
}
public final class ExerciseEventRecord implements androidx.health.connect.client.records.IntervalRecord {
- ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public ExerciseEventRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int eventType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
- method public String getEventType();
+ method public int getEventType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
- property public final String eventType;
+ property public final int eventType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
+ field public static final androidx.health.connect.client.records.ExerciseEventRecord.Companion Companion;
+ field public static final int EVENT_TYPE_PAUSE = 1; // 0x1
+ field public static final int EVENT_TYPE_REST = 2; // 0x2
+ field public static final int EVENT_TYPE_UNKNOWN = 0; // 0x0
}
- public static final class ExerciseEventRecord.EventType {
- field public static final androidx.health.connect.client.records.ExerciseEventRecord.EventType INSTANCE;
- field public static final String PAUSE = "pause";
- field public static final String REST = "rest";
+ public static final class ExerciseEventRecord.Companion {
}
public final class ExerciseLapRecord implements androidx.health.connect.client.records.IntervalRecord {
@@ -729,12 +731,12 @@
}
public final class MealType {
- field public static final String BREAKFAST = "breakfast";
- field public static final String DINNER = "dinner";
field public static final androidx.health.connect.client.records.MealType INSTANCE;
- field public static final String LUNCH = "lunch";
- field public static final String SNACK = "snack";
- field public static final String UNKNOWN = "unknown";
+ field public static final int MEAL_TYPE_BREAKFAST = 1; // 0x1
+ field public static final int MEAL_TYPE_DINNER = 3; // 0x3
+ field public static final int MEAL_TYPE_LUNCH = 2; // 0x2
+ field public static final int MEAL_TYPE_SNACK = 4; // 0x4
+ field public static final int MEAL_TYPE_UNKNOWN = 0; // 0x0
}
public final class MenstruationFlowRecord implements androidx.health.connect.client.records.InstantaneousRecord {
@@ -758,7 +760,7 @@
}
public final class NutritionRecord implements androidx.health.connect.client.records.IntervalRecord {
- ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional String? mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public NutritionRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, optional androidx.health.connect.client.units.Mass? biotin, optional androidx.health.connect.client.units.Mass? caffeine, optional androidx.health.connect.client.units.Mass? calcium, optional androidx.health.connect.client.units.Energy? energy, optional androidx.health.connect.client.units.Energy? energyFromFat, optional androidx.health.connect.client.units.Mass? chloride, optional androidx.health.connect.client.units.Mass? cholesterol, optional androidx.health.connect.client.units.Mass? chromium, optional androidx.health.connect.client.units.Mass? copper, optional androidx.health.connect.client.units.Mass? dietaryFiber, optional androidx.health.connect.client.units.Mass? folate, optional androidx.health.connect.client.units.Mass? folicAcid, optional androidx.health.connect.client.units.Mass? iodine, optional androidx.health.connect.client.units.Mass? iron, optional androidx.health.connect.client.units.Mass? magnesium, optional androidx.health.connect.client.units.Mass? manganese, optional androidx.health.connect.client.units.Mass? molybdenum, optional androidx.health.connect.client.units.Mass? monounsaturatedFat, optional androidx.health.connect.client.units.Mass? niacin, optional androidx.health.connect.client.units.Mass? pantothenicAcid, optional androidx.health.connect.client.units.Mass? phosphorus, optional androidx.health.connect.client.units.Mass? polyunsaturatedFat, optional androidx.health.connect.client.units.Mass? potassium, optional androidx.health.connect.client.units.Mass? protein, optional androidx.health.connect.client.units.Mass? riboflavin, optional androidx.health.connect.client.units.Mass? saturatedFat, optional androidx.health.connect.client.units.Mass? selenium, optional androidx.health.connect.client.units.Mass? sodium, optional androidx.health.connect.client.units.Mass? sugar, optional androidx.health.connect.client.units.Mass? thiamin, optional androidx.health.connect.client.units.Mass? totalCarbohydrate, optional androidx.health.connect.client.units.Mass? totalFat, optional androidx.health.connect.client.units.Mass? transFat, optional androidx.health.connect.client.units.Mass? unsaturatedFat, optional androidx.health.connect.client.units.Mass? vitaminA, optional androidx.health.connect.client.units.Mass? vitaminB12, optional androidx.health.connect.client.units.Mass? vitaminB6, optional androidx.health.connect.client.units.Mass? vitaminC, optional androidx.health.connect.client.units.Mass? vitaminD, optional androidx.health.connect.client.units.Mass? vitaminE, optional androidx.health.connect.client.units.Mass? vitaminK, optional androidx.health.connect.client.units.Mass? zinc, optional String? name, optional int mealType, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.units.Mass? getBiotin();
method public androidx.health.connect.client.units.Mass? getCaffeine();
method public androidx.health.connect.client.units.Mass? getCalcium();
@@ -777,7 +779,7 @@
method public androidx.health.connect.client.units.Mass? getIron();
method public androidx.health.connect.client.units.Mass? getMagnesium();
method public androidx.health.connect.client.units.Mass? getManganese();
- method public String? getMealType();
+ method public int getMealType();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public androidx.health.connect.client.units.Mass? getMolybdenum();
method public androidx.health.connect.client.units.Mass? getMonounsaturatedFat();
@@ -826,7 +828,7 @@
property public final androidx.health.connect.client.units.Mass? iron;
property public final androidx.health.connect.client.units.Mass? magnesium;
property public final androidx.health.connect.client.units.Mass? manganese;
- property public final String? mealType;
+ property public final int mealType;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public final androidx.health.connect.client.units.Mass? molybdenum;
property public final androidx.health.connect.client.units.Mass? monounsaturatedFat;
@@ -906,23 +908,23 @@
}
public final class OvulationTestRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, String result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public OvulationTestRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, int result, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String getResult();
+ method public int getResult();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String result;
+ property public final int result;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.OvulationTestRecord.Companion Companion;
+ field public static final int RESULT_HIGH = 2; // 0x2
+ field public static final int RESULT_INCONCLUSIVE = 0; // 0x0
+ field public static final int RESULT_NEGATIVE = 3; // 0x3
+ field public static final int RESULT_POSITIVE = 1; // 0x1
}
- public static final class OvulationTestRecord.Result {
- field public static final String HIGH = "high";
- field public static final String INCONCLUSIVE = "inconclusive";
- field public static final androidx.health.connect.client.records.OvulationTestRecord.Result INSTANCE;
- field public static final String NEGATIVE = "negative";
- field public static final String POSITIVE = "positive";
+ public static final class OvulationTestRecord.Companion {
}
public final class OxygenSaturationRecord implements androidx.health.connect.client.records.InstantaneousRecord {
@@ -973,14 +975,6 @@
property public abstract androidx.health.connect.client.records.metadata.Metadata metadata;
}
- public final class RelationToMeal {
- field public static final String AFTER_MEAL = "after_meal";
- field public static final String BEFORE_MEAL = "before_meal";
- field public static final String FASTING = "fasting";
- field public static final String GENERAL = "general";
- field public static final androidx.health.connect.client.records.RelationToMeal INSTANCE;
- }
-
public final class RespiratoryRateRecord implements androidx.health.connect.client.records.InstantaneousRecord {
ctor public RespiratoryRateRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double rate, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
@@ -1018,21 +1012,22 @@
}
public final class SexualActivityRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SexualActivityRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int protectionUsed, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
- method public String? getProtectionUsed();
+ method public int getProtectionUsed();
method public java.time.Instant getTime();
method public java.time.ZoneOffset? getZoneOffset();
property public androidx.health.connect.client.records.metadata.Metadata metadata;
- property public final String? protectionUsed;
+ property public final int protectionUsed;
property public java.time.Instant time;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.SexualActivityRecord.Companion Companion;
+ field public static final int PROTECTION_USED_PROTECTED = 1; // 0x1
+ field public static final int PROTECTION_USED_UNKNOWN = 0; // 0x0
+ field public static final int PROTECTION_USED_UNPROTECTED = 2; // 0x2
}
- public static final class SexualActivityRecord.Protection {
- field public static final androidx.health.connect.client.records.SexualActivityRecord.Protection INSTANCE;
- field public static final String PROTECTED = "protected";
- field public static final String UNPROTECTED = "unprotected";
+ public static final class SexualActivityRecord.Companion {
}
public final class SleepSessionRecord implements androidx.health.connect.client.records.IntervalRecord {
@@ -1169,31 +1164,31 @@
}
public final class SwimmingStrokesRecord implements androidx.health.connect.client.records.IntervalRecord {
- ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, String type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ ctor public SwimmingStrokesRecord(java.time.Instant startTime, java.time.ZoneOffset? startZoneOffset, java.time.Instant endTime, java.time.ZoneOffset? endZoneOffset, int type, optional long count, optional androidx.health.connect.client.records.metadata.Metadata metadata);
method public long getCount();
method public java.time.Instant getEndTime();
method public java.time.ZoneOffset? getEndZoneOffset();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getStartTime();
method public java.time.ZoneOffset? getStartZoneOffset();
- method public String getType();
+ method public int getType();
property public final long count;
property public java.time.Instant endTime;
property public java.time.ZoneOffset? endZoneOffset;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant startTime;
property public java.time.ZoneOffset? startZoneOffset;
- property public final String type;
+ property public final int type;
+ field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.Companion Companion;
+ field public static final int SWIMMING_TYPE_BACKSTROKE = 2; // 0x2
+ field public static final int SWIMMING_TYPE_BREASTSTROKE = 3; // 0x3
+ field public static final int SWIMMING_TYPE_BUTTERFLY = 4; // 0x4
+ field public static final int SWIMMING_TYPE_FREESTYLE = 1; // 0x1
+ field public static final int SWIMMING_TYPE_MIXED = 5; // 0x5
+ field public static final int SWIMMING_TYPE_OTHER = 0; // 0x0
}
- public static final class SwimmingStrokesRecord.SwimmingType {
- field public static final String BACKSTROKE = "backstroke";
- field public static final String BREASTSTROKE = "breaststroke";
- field public static final String BUTTERFLY = "butterfly";
- field public static final String FREESTYLE = "freestyle";
- field public static final androidx.health.connect.client.records.SwimmingStrokesRecord.SwimmingType INSTANCE;
- field public static final String MIXED = "mixed";
- field public static final String OTHER = "other";
+ public static final class SwimmingStrokesRecord.Companion {
}
public final class TotalCaloriesBurnedRecord implements androidx.health.connect.client.records.IntervalRecord {
@@ -1218,27 +1213,27 @@
}
public final class Vo2MaxRecord implements androidx.health.connect.client.records.InstantaneousRecord {
- ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional String? measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
- method public String? getMeasurementMethod();
+ ctor public Vo2MaxRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, double vo2MillilitersPerMinuteKilogram, optional int measurementMethod, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+ method public int getMeasurementMethod();
method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
method public java.time.Instant getTime();
method public double getVo2MillilitersPerMinuteKilogram();
method public java.time.ZoneOffset? getZoneOffset();
- property public final String? measurementMethod;
+ property public final int measurementMethod;
property public androidx.health.connect.client.records.metadata.Metadata metadata;
property public java.time.Instant time;
property public final double vo2MillilitersPerMinuteKilogram;
property public java.time.ZoneOffset? zoneOffset;
+ field public static final androidx.health.connect.client.records.Vo2MaxRecord.Companion Companion;
+ field public static final int MEASUREMENT_METHOD_COOPER_TEST = 3; // 0x3
+ field public static final int MEASUREMENT_METHOD_HEART_RATE_RATIO = 2; // 0x2
+ field public static final int MEASUREMENT_METHOD_METABOLIC_CART = 1; // 0x1
+ field public static final int MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST = 4; // 0x4
+ field public static final int MEASUREMENT_METHOD_OTHER = 0; // 0x0
+ field public static final int MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST = 5; // 0x5
}
- public static final class Vo2MaxRecord.MeasurementMethod {
- field public static final String COOPER_TEST = "cooper_test";
- field public static final String HEART_RATE_RATIO = "heart_rate_ratio";
- field public static final androidx.health.connect.client.records.Vo2MaxRecord.MeasurementMethod INSTANCE;
- field public static final String METABOLIC_CART = "metabolic_cart";
- field public static final String MULTISTAGE_FITNESS_TEST = "multistage_fitness_test";
- field public static final String OTHER = "other";
- field public static final String ROCKPORT_FITNESS_TEST = "rockport_fitness_test";
+ public static final class Vo2MaxRecord.Companion {
}
public final class WaistCircumferenceRecord implements androidx.health.connect.client.records.InstantaneousRecord {
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/impl/converters/records/ProtoToRecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index 8932e63..8330756 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -24,6 +24,7 @@
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.BodyTemperatureMeasurementLocation
import androidx.health.connect.client.records.BodyTemperatureRecord
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
@@ -53,6 +54,7 @@
import androidx.health.connect.client.records.HydrationRecord
import androidx.health.connect.client.records.IntermenstrualBleedingRecord
import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MealType
import androidx.health.connect.client.records.MenstruationFlowRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.OvulationTestRecord
@@ -95,7 +97,13 @@
"BasalBodyTemperature" ->
BasalBodyTemperatureRecord(
temperature = getDouble("temperature").celsius,
- measurementLocation = getEnum("measurementLocation"),
+ measurementLocation =
+ mapEnum(
+ "measurementLocation",
+ BodyTemperatureMeasurementLocation
+ .MEASUREMENT_LOCATION_STRING_TO_INT_MAP,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN,
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -110,9 +118,24 @@
"BloodGlucose" ->
BloodGlucoseRecord(
level = BloodGlucose.millimolesPerLiter(getDouble("level")),
- specimenSource = getEnum("specimenSource"),
- mealType = getEnum("mealType"),
- relationToMeal = getEnum("relationToMeal"),
+ specimenSource =
+ mapEnum(
+ "specimenSource",
+ BloodGlucoseRecord.SPECIMEN_SOURCE_STRING_TO_INT_MAP,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_UNKNOWN
+ ),
+ mealType =
+ mapEnum(
+ "mealType",
+ MealType.MEAL_TYPE_STRING_TO_INT_MAP,
+ MealType.MEAL_TYPE_UNKNOWN
+ ),
+ relationToMeal =
+ mapEnum(
+ "relationToMeal",
+ BloodGlucoseRecord.RELATION_TO_MEAL_STRING_TO_INT_MAP,
+ BloodGlucoseRecord.RELATION_TO_MEAL_UNKNOWN
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -121,8 +144,18 @@
BloodPressureRecord(
systolic = getDouble("systolic").millimetersOfMercury,
diastolic = getDouble("diastolic").millimetersOfMercury,
- bodyPosition = getEnum("bodyPosition"),
- measurementLocation = getEnum("measurementLocation"),
+ bodyPosition =
+ mapEnum(
+ "bodyPosition",
+ BloodPressureRecord.BODY_POSITION_STRING_TO_INT_MAP,
+ BloodPressureRecord.BODY_POSITION_UNKNOWN
+ ),
+ measurementLocation =
+ mapEnum(
+ "measurementLocation",
+ BloodPressureRecord.MEASUREMENT_LOCATION_STRING_TO_INT_MAP,
+ BloodPressureRecord.MEASUREMENT_LOCATION_UNKNOWN
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -137,7 +170,13 @@
"BodyTemperature" ->
BodyTemperatureRecord(
temperature = getDouble("temperature").celsius,
- measurementLocation = getEnum("measurementLocation"),
+ measurementLocation =
+ mapEnum(
+ "measurementLocation",
+ BodyTemperatureMeasurementLocation
+ .MEASUREMENT_LOCATION_STRING_TO_INT_MAP,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -302,7 +341,12 @@
)
"OvulationTest" ->
OvulationTestRecord(
- result = getEnum("result") ?: "",
+ result =
+ mapEnum(
+ "result",
+ OvulationTestRecord.RESULT_STRING_TO_INT_MAP,
+ OvulationTestRecord.RESULT_INCONCLUSIVE
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -345,7 +389,12 @@
)
"SexualActivity" ->
SexualActivityRecord(
- protectionUsed = getEnum("protectionUsed"),
+ protectionUsed =
+ mapEnum(
+ "protectionUsed",
+ SexualActivityRecord.PROTECTION_USED_STRING_TO_INT_MAP,
+ SexualActivityRecord.PROTECTION_USED_UNKNOWN
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -383,7 +432,12 @@
"Vo2Max" ->
Vo2MaxRecord(
vo2MillilitersPerMinuteKilogram = getDouble("vo2"),
- measurementMethod = getEnum("measurementMethod"),
+ measurementMethod =
+ mapEnum(
+ "measurementMethod",
+ Vo2MaxRecord.MEASUREMENT_METHOD_STRING_TO_INT_MAP,
+ Vo2MaxRecord.MEASUREMENT_METHOD_OTHER
+ ),
time = time,
zoneOffset = zoneOffset,
metadata = metadata
@@ -413,7 +467,12 @@
)
"ActivityEvent" ->
ExerciseEventRecord(
- eventType = getEnum("eventType") ?: "",
+ eventType =
+ mapEnum(
+ "eventType",
+ ExerciseEventRecord.EVENT_TYPE_STRING_TO_INT_MAP,
+ ExerciseEventRecord.EVENT_TYPE_UNKNOWN,
+ ),
startTime = startTime,
startZoneOffset = startZoneOffset,
endTime = endTime,
@@ -525,7 +584,12 @@
vitaminE = valuesMap["vitaminE"]?.doubleVal?.grams,
vitaminK = valuesMap["vitaminK"]?.doubleVal?.grams,
zinc = valuesMap["zinc"]?.doubleVal?.grams,
- mealType = getEnum("mealType"),
+ mealType =
+ mapEnum(
+ "mealType",
+ MealType.MEAL_TYPE_STRING_TO_INT_MAP,
+ MealType.MEAL_TYPE_UNKNOWN
+ ),
name = getString("name"),
startTime = startTime,
startZoneOffset = startZoneOffset,
@@ -590,7 +654,12 @@
"SwimmingStrokes" ->
SwimmingStrokesRecord(
count = getLong("count"),
- type = getEnum("type") ?: "",
+ type =
+ mapEnum(
+ "type",
+ SwimmingStrokesRecord.SWIMMING_TYPE_STRING_TO_INT_MAP,
+ SwimmingStrokesRecord.SWIMMING_TYPE_OTHER
+ ),
startTime = startTime,
startZoneOffset = startZoneOffset,
endTime = endTime,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index 0f78a92..aa31176 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -24,6 +24,7 @@
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.BodyTemperatureMeasurementLocation
import androidx.health.connect.client.records.BodyTemperatureRecord
import androidx.health.connect.client.records.BodyWaterMassRecord
import androidx.health.connect.client.records.BoneMassRecord
@@ -53,6 +54,7 @@
import androidx.health.connect.client.records.HydrationRecord
import androidx.health.connect.client.records.IntermenstrualBleedingRecord
import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MealType
import androidx.health.connect.client.records.MenstruationFlowRecord
import androidx.health.connect.client.records.NutritionRecord
import androidx.health.connect.client.records.OvulationTestRecord
@@ -85,7 +87,12 @@
.setDataType(protoDataType("BasalBodyTemperature"))
.apply {
putValues("temperature", doubleVal(temperature.inCelsius))
- measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
+ enumValFromInt(
+ measurementLocation,
+ BodyTemperatureMeasurementLocation
+ .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
+ )
+ ?.let { putValues("measurementLocation", it) }
}
.build()
is BasalMetabolicRateRecord ->
@@ -98,9 +105,19 @@
.setDataType(protoDataType("BloodGlucose"))
.apply {
putValues("level", doubleVal(level.inMillimolesPerLiter))
- specimenSource?.let { putValues("specimenSource", enumVal(it)) }
- mealType?.let { putValues("mealType", enumVal(it)) }
- relationToMeal?.let { putValues("relationToMeal", enumVal(it)) }
+ enumValFromInt(
+ specimenSource,
+ BloodGlucoseRecord.SPECIMEN_SOURCE_INT_TO_STRING_MAP
+ )
+ ?.let { putValues("specimenSource", it) }
+ enumValFromInt(mealType, MealType.MEAL_TYPE_INT_TO_STRING_MAP)?.let {
+ putValues("mealType", it)
+ }
+ enumValFromInt(
+ relationToMeal,
+ BloodGlucoseRecord.RELATION_TO_MEAL_INT_TO_STRING_MAP,
+ )
+ ?.let { putValues("relationToMeal", it) }
}
.build()
is BloodPressureRecord ->
@@ -109,8 +126,16 @@
.apply {
putValues("systolic", doubleVal(systolic.inMillimetersOfMercury))
putValues("diastolic", doubleVal(diastolic.inMillimetersOfMercury))
- bodyPosition?.let { putValues("bodyPosition", enumVal(it)) }
- measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
+ enumValFromInt(
+ bodyPosition,
+ BloodPressureRecord.BODY_POSITION_INT_TO_STRING_MAP
+ )
+ ?.let { putValues("bodyPosition", it) }
+ enumValFromInt(
+ measurementLocation,
+ BloodPressureRecord.MEASUREMENT_LOCATION_INT_TO_STRING_MAP
+ )
+ ?.let { putValues("measurementLocation", it) }
}
.build()
is BodyFatRecord ->
@@ -123,7 +148,12 @@
.setDataType(protoDataType("BodyTemperature"))
.apply {
putValues("temperature", doubleVal(temperature.inCelsius))
- measurementLocation?.let { putValues("measurementLocation", enumVal(it)) }
+ enumValFromInt(
+ measurementLocation,
+ BodyTemperatureMeasurementLocation
+ .MEASUREMENT_LOCATION_INT_TO_STRING_MAP,
+ )
+ ?.let { putValues("measurementLocation", it) }
}
.build()
is BodyWaterMassRecord ->
@@ -236,7 +266,11 @@
is OvulationTestRecord ->
instantaneousProto()
.setDataType(protoDataType("OvulationTest"))
- .apply { putValues("result", enumVal(result)) }
+ .apply {
+ enumValFromInt(result, OvulationTestRecord.RESULT_INT_TO_STRING_MAP)?.let {
+ putValues("result", it)
+ }
+ }
.build()
is OxygenSaturationRecord ->
instantaneousProto()
@@ -263,7 +297,13 @@
is SexualActivityRecord ->
instantaneousProto()
.setDataType(protoDataType("SexualActivity"))
- .apply { protectionUsed?.let { putValues("protectionUsed", enumVal(it)) } }
+ .apply {
+ enumValFromInt(
+ protectionUsed,
+ SexualActivityRecord.PROTECTION_USED_INT_TO_STRING_MAP
+ )
+ ?.let { putValues("protectionUsed", it) }
+ }
.build()
is SpeedRecord ->
toProto(dataTypeName = "SpeedSeries") { sample ->
@@ -284,7 +324,11 @@
.setDataType(protoDataType("Vo2Max"))
.apply {
putValues("vo2", doubleVal(vo2MillilitersPerMinuteKilogram))
- measurementMethod?.let { putValues("measurementMethod", enumVal(it)) }
+ enumValFromInt(
+ measurementMethod,
+ Vo2MaxRecord.MEASUREMENT_METHOD_INT_TO_STRING_MAP
+ )
+ ?.let { putValues("measurementMethod", it) }
}
.build()
is WaistCircumferenceRecord ->
@@ -305,7 +349,10 @@
is ExerciseEventRecord ->
intervalProto()
.setDataType(protoDataType("ActivityEvent"))
- .apply { putValues("eventType", enumVal(eventType)) }
+ .apply {
+ enumValFromInt(eventType, ExerciseEventRecord.EVENT_TYPE_INT_TO_STRING_MAP)
+ ?.let { putValues("eventType", it) }
+ }
.build()
is ExerciseLapRecord ->
intervalProto()
@@ -481,7 +528,9 @@
if (zinc != null) {
putValues("zinc", doubleVal(zinc.inGrams))
}
- mealType?.let { putValues("mealType", enumVal(it)) }
+ enumValFromInt(mealType, MealType.MEAL_TYPE_INT_TO_STRING_MAP)?.let {
+ putValues("mealType", it)
+ }
name?.let { putValues("name", stringVal(it)) }
}
.build()
@@ -526,7 +575,10 @@
if (count > 0) {
putValues("count", longVal(count))
}
- putValues("type", enumVal(type))
+ val swimmingType =
+ enumValFromInt(type, SwimmingStrokesRecord.SWIMMING_TYPE_INT_TO_STRING_MAP)
+ ?: enumVal(SwimmingStrokesRecord.SwimmingType.OTHER)
+ putValues("type", swimmingType)
}
.build()
is TotalCaloriesBurnedRecord ->
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 6c9fffd..2a787eb 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 =
@@ -236,6 +323,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/BasalBodyTemperatureRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
index 489bf11..57e308b 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BasalBodyTemperatureRecord.kt
@@ -37,7 +37,9 @@
*
* @see BodyTemperatureMeasurementLocation
*/
- @property:BodyTemperatureMeasurementLocations public val measurementLocation: String? = null,
+ @property:BodyTemperatureMeasurementLocations
+ public val measurementLocation: Int =
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
@@ -67,7 +69,7 @@
*/
override fun hashCode(): Int {
var result = temperature.hashCode()
- result = 31 * result + (measurementLocation?.hashCode() ?: 0)
+ result = 31 * result + measurementLocation
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
index 973ff0f..1b74254 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
@@ -42,21 +42,21 @@
*
* @see SpecimenSource
*/
- @property:SpecimenSources public val specimenSource: String? = null,
+ @property:SpecimenSources public val specimenSource: Int = SPECIMEN_SOURCE_UNKNOWN,
/**
* Type of meal related to the blood glucose measurement. Optional, enum field. Allowed values:
* [MealType].
*
* @see MealType
*/
- @property:MealTypes public val mealType: String? = null,
+ @property:MealTypes public val mealType: Int = MealType.MEAL_TYPE_UNKNOWN,
/**
* Relationship of the meal to the blood glucose measurement. Optional, enum field. Allowed
* values: [RelationToMeal].
*
* @see RelationToMeal
*/
- @property:RelationToMeals public val relationToMeal: String? = null,
+ @property:RelationToMeals public val relationToMeal: Int = RELATION_TO_MEAL_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
@@ -65,43 +65,11 @@
level.requireNotMore(other = MAX_BLOOD_GLUCOSE_LEVEL, name = "level")
}
- /*
- * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
- */
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (other !is BloodGlucoseRecord) return false
-
- if (level != other.level) return false
- if (specimenSource != other.specimenSource) return false
- if (mealType != other.mealType) return false
- if (relationToMeal != other.relationToMeal) return false
- if (time != other.time) return false
- if (zoneOffset != other.zoneOffset) return false
- if (metadata != other.metadata) return false
-
- return true
- }
-
- /*
- * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
- */
- override fun hashCode(): Int {
- var result = level.hashCode()
- result = 31 * result + (specimenSource?.hashCode() ?: 0)
- result = 31 * result + (mealType?.hashCode() ?: 0)
- result = 31 * result + (relationToMeal?.hashCode() ?: 0)
- result = 31 * result + time.hashCode()
- result = 31 * result + (zoneOffset?.hashCode() ?: 0)
- result = 31 * result + metadata.hashCode()
- return result
- }
-
/**
* List of supported blood glucose specimen sources (type of body fluid used to measure the
* blood glucose).
*/
- object SpecimenSource {
+ internal object SpecimenSource {
const val INTERSTITIAL_FLUID = "interstitial_fluid"
const val CAPILLARY_BLOOD = "capillary_blood"
const val PLASMA = "plasma"
@@ -110,6 +78,62 @@
const val WHOLE_BLOOD = "whole_blood"
}
+ /** Temporal relationship of measurement time to a meal. */
+ internal object RelationToMeal {
+ const val GENERAL = "general"
+ const val FASTING = "fasting"
+ const val BEFORE_MEAL = "before_meal"
+ const val AFTER_MEAL = "after_meal"
+ }
+
+ companion object {
+ private val MAX_BLOOD_GLUCOSE_LEVEL = BloodGlucose.millimolesPerLiter(50.0)
+
+ const val RELATION_TO_MEAL_UNKNOWN = 0
+ const val RELATION_TO_MEAL_GENERAL = 1
+ const val RELATION_TO_MEAL_FASTING = 2
+ const val RELATION_TO_MEAL_BEFORE_MEAL = 3
+ const val RELATION_TO_MEAL_AFTER_MEAL = 4
+
+ const val SPECIMEN_SOURCE_UNKNOWN = 0
+ const val SPECIMEN_SOURCE_INTERSTITIAL_FLUID = 1
+ const val SPECIMEN_SOURCE_CAPILLARY_BLOOD = 2
+ const val SPECIMEN_SOURCE_PLASMA = 3
+ const val SPECIMEN_SOURCE_SERUM = 4
+ const val SPECIMEN_SOURCE_TEARS = 5
+ const val SPECIMEN_SOURCE_WHOLE_BLOOD = 6
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val RELATION_TO_MEAL_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ RelationToMeal.GENERAL to RELATION_TO_MEAL_GENERAL,
+ RelationToMeal.AFTER_MEAL to RELATION_TO_MEAL_AFTER_MEAL,
+ RelationToMeal.FASTING to RELATION_TO_MEAL_FASTING,
+ RelationToMeal.BEFORE_MEAL to RELATION_TO_MEAL_BEFORE_MEAL
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val RELATION_TO_MEAL_INT_TO_STRING_MAP = RELATION_TO_MEAL_STRING_TO_INT_MAP.reverse()
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SPECIMEN_SOURCE_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ SpecimenSource.INTERSTITIAL_FLUID to SPECIMEN_SOURCE_INTERSTITIAL_FLUID,
+ SpecimenSource.CAPILLARY_BLOOD to SPECIMEN_SOURCE_CAPILLARY_BLOOD,
+ SpecimenSource.PLASMA to SPECIMEN_SOURCE_PLASMA,
+ SpecimenSource.TEARS to SPECIMEN_SOURCE_TEARS,
+ SpecimenSource.WHOLE_BLOOD to SPECIMEN_SOURCE_WHOLE_BLOOD,
+ SpecimenSource.SERUM to SPECIMEN_SOURCE_SERUM
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SPECIMEN_SOURCE_INT_TO_STRING_MAP = SPECIMEN_SOURCE_STRING_TO_INT_MAP.reverse()
+ }
+
/**
* List of supported blood glucose specimen sources (type of body fluid used to measure the
* blood glucose).
@@ -131,7 +155,50 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class SpecimenSources
- private companion object {
- private val MAX_BLOOD_GLUCOSE_LEVEL = BloodGlucose.millimolesPerLiter(50.0)
+ /**
+ * Temporal relationship of measurement time to a meal.
+ * @suppress
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @StringDef(
+ value =
+ [
+ RelationToMeal.GENERAL,
+ RelationToMeal.FASTING,
+ RelationToMeal.BEFORE_MEAL,
+ RelationToMeal.AFTER_MEAL,
+ ]
+ )
+ annotation class RelationToMeals
+
+ /*
+ * Generated by the IDE: Code -> Generate -> "equals() and hashCode()".
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as BloodGlucoseRecord
+
+ if (time != other.time) return false
+ if (zoneOffset != other.zoneOffset) return false
+ if (level != other.level) return false
+ if (specimenSource != other.specimenSource) return false
+ if (mealType != other.mealType) return false
+ if (relationToMeal != other.relationToMeal) return false
+ if (metadata != other.metadata) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = time.hashCode()
+ result = 31 * result + (zoneOffset?.hashCode() ?: 0)
+ result = 31 * result + level.hashCode()
+ result = 31 * result + specimenSource
+ result = 31 * result + mealType
+ result = 31 * result + relationToMeal
+ result = 31 * result + metadata.hashCode()
+ return result
}
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
index 08f023d..3ba3e7a 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodPressureRecord.kt
@@ -15,7 +15,8 @@
*/
package androidx.health.connect.client.records
-import androidx.annotation.StringDef
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.aggregate.AggregateMetric
import androidx.health.connect.client.records.BloodPressureRecord.MeasurementLocation
import androidx.health.connect.client.records.metadata.Metadata
@@ -47,14 +48,15 @@
*
* @see BodyPosition
*/
- @property:BodyPositions public val bodyPosition: String? = null,
+ @property:BodyPositions public val bodyPosition: Int = BODY_POSITION_UNKNOWN,
/**
* The arm and part of the arm where the measurement was taken. Optional field. Allowed values:
* [MeasurementLocation].
*
* @see MeasurementLocation
*/
- @property:MeasurementLocations public val measurementLocation: String? = null,
+ @property:MeasurementLocations
+ public val measurementLocation: Int = MEASUREMENT_LOCATION_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
@@ -89,8 +91,8 @@
override fun hashCode(): Int {
var result = systolic.hashCode()
result = 31 * result + diastolic.hashCode()
- result = 31 * result + (bodyPosition?.hashCode() ?: 0)
- result = 31 * result + (measurementLocation?.hashCode() ?: 0)
+ result = 31 * result + bodyPosition
+ result = 31 * result + measurementLocation
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
@@ -98,7 +100,7 @@
}
/** The arm and part of the arm where a blood pressure measurement was taken. */
- public object MeasurementLocation {
+ internal object MeasurementLocation {
const val LEFT_WRIST = "left_wrist"
const val RIGHT_WRIST = "right_wrist"
const val LEFT_UPPER_ARM = "left_upper_arm"
@@ -106,22 +108,93 @@
}
/**
+ * The user's body position when a health measurement is taken.
+ * @suppress
+ */
+ internal object BodyPosition {
+ const val STANDING_UP = "standing_up"
+ const val SITTING_DOWN = "sitting_down"
+ const val LYING_DOWN = "lying_down"
+ const val RECLINING = "reclining"
+ }
+
+ /**
* The arm and part of the arm where a blood pressure measurement was taken.
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- MeasurementLocation.LEFT_WRIST,
- MeasurementLocation.RIGHT_WRIST,
- MeasurementLocation.LEFT_UPPER_ARM,
- MeasurementLocation.RIGHT_UPPER_ARM,
+ MEASUREMENT_LOCATION_UNKNOWN,
+ MEASUREMENT_LOCATION_LEFT_WRIST,
+ MEASUREMENT_LOCATION_RIGHT_WRIST,
+ MEASUREMENT_LOCATION_LEFT_UPPER_ARM,
+ MEASUREMENT_LOCATION_RIGHT_UPPER_ARM
]
)
annotation class MeasurementLocations
+ /**
+ * The user's body position when a health measurement is taken.
+ * @suppress
+ */
+ @Retention(AnnotationRetention.SOURCE)
+ @IntDef(
+ value =
+ [
+ BODY_POSITION_UNKNOWN,
+ BODY_POSITION_STANDING_UP,
+ BODY_POSITION_SITTING_DOWN,
+ BODY_POSITION_LYING_DOWN,
+ BODY_POSITION_RECLINING
+ ]
+ )
+ annotation class BodyPositions
+
companion object {
+
+ const val MEASUREMENT_LOCATION_UNKNOWN = 0
+ const val MEASUREMENT_LOCATION_LEFT_WRIST = 1
+ const val MEASUREMENT_LOCATION_RIGHT_WRIST = 2
+ const val MEASUREMENT_LOCATION_LEFT_UPPER_ARM = 3
+ const val MEASUREMENT_LOCATION_RIGHT_UPPER_ARM = 4
+
+ const val BODY_POSITION_UNKNOWN = 0
+ const val BODY_POSITION_STANDING_UP = 1
+ const val BODY_POSITION_SITTING_DOWN = 2
+ const val BODY_POSITION_LYING_DOWN = 3
+ const val BODY_POSITION_RECLINING = 4
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_LOCATION_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ MeasurementLocation.LEFT_UPPER_ARM to MEASUREMENT_LOCATION_LEFT_UPPER_ARM,
+ MeasurementLocation.LEFT_WRIST to MEASUREMENT_LOCATION_LEFT_WRIST,
+ MeasurementLocation.RIGHT_UPPER_ARM to MEASUREMENT_LOCATION_RIGHT_UPPER_ARM,
+ MeasurementLocation.RIGHT_WRIST to MEASUREMENT_LOCATION_RIGHT_WRIST
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_LOCATION_INT_TO_STRING_MAP =
+ MEASUREMENT_LOCATION_STRING_TO_INT_MAP.reverse()
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val BODY_POSITION_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ BodyPosition.LYING_DOWN to BODY_POSITION_LYING_DOWN,
+ BodyPosition.RECLINING to BODY_POSITION_RECLINING,
+ BodyPosition.SITTING_DOWN to BODY_POSITION_SITTING_DOWN,
+ BodyPosition.STANDING_UP to BODY_POSITION_STANDING_UP
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val BODY_POSITION_INT_TO_STRING_MAP = BODY_POSITION_STRING_TO_INT_MAP.reverse()
+
private const val BLOOD_PRESSURE_NAME = "BloodPressure"
private const val SYSTOLIC_FIELD_NAME = "systolic"
private const val DIASTOLIC_FIELD_NAME = "diastolic"
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyPosition.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyPosition.kt
deleted file mode 100644
index 96c7662..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyPosition.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.StringDef
-
-/** The user's body position when a health measurement is taken. */
-public object BodyPosition {
- const val STANDING_UP = "standing_up"
- const val SITTING_DOWN = "sitting_down"
- const val LYING_DOWN = "lying_down"
- const val RECLINING = "reclining"
-}
-
-/**
- * The user's body position when a health measurement is taken.
- * @suppress
- */
-@Retention(AnnotationRetention.SOURCE)
-@StringDef(
- value =
- [
- BodyPosition.STANDING_UP,
- BodyPosition.SITTING_DOWN,
- BodyPosition.LYING_DOWN,
- BodyPosition.RECLINING,
- ]
-)
-annotation class BodyPositions
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
index 949d055..6b77845 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocation.kt
@@ -15,21 +15,54 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
/** Where on the user's body a temperature measurement was taken from. */
public object BodyTemperatureMeasurementLocation {
- const val ARMPIT = "armpit"
- const val FINGER = "finger"
- const val FOREHEAD = "forehead"
- const val MOUTH = "mouth"
- const val RECTUM = "rectum"
- const val TEMPORAL_ARTERY = "temporal_artery"
- const val TOE = "toe"
- const val EAR = "ear"
- const val WRIST = "wrist"
- const val VAGINA = "vagina"
+ const val MEASUREMENT_LOCATION_UNKNOWN = 0
+ const val MEASUREMENT_LOCATION_ARMPIT = 1
+ const val MEASUREMENT_LOCATION_FINGER = 2
+ const val MEASUREMENT_LOCATION_FOREHEAD = 3
+ const val MEASUREMENT_LOCATION_MOUTH = 4
+ const val MEASUREMENT_LOCATION_RECTUM = 5
+ const val MEASUREMENT_LOCATION_TEMPORAL_ARTERY = 6
+ const val MEASUREMENT_LOCATION_TOE = 7
+ const val MEASUREMENT_LOCATION_EAR = 8
+ const val MEASUREMENT_LOCATION_WRIST = 9
+ const val MEASUREMENT_LOCATION_VAGINA = 10
+
+ internal const val ARMPIT = "armpit"
+ internal const val FINGER = "finger"
+ internal const val FOREHEAD = "forehead"
+ internal const val MOUTH = "mouth"
+ internal const val RECTUM = "rectum"
+ internal const val TEMPORAL_ARTERY = "temporal_artery"
+ internal const val TOE = "toe"
+ internal const val EAR = "ear"
+ internal const val WRIST = "wrist"
+ internal const val VAGINA = "vagina"
+
+ /** Internal mappings useful for interoperability between integers and strings. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_LOCATION_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ ARMPIT to MEASUREMENT_LOCATION_ARMPIT,
+ FINGER to MEASUREMENT_LOCATION_FINGER,
+ FOREHEAD to MEASUREMENT_LOCATION_FOREHEAD,
+ MOUTH to MEASUREMENT_LOCATION_MOUTH,
+ RECTUM to MEASUREMENT_LOCATION_RECTUM,
+ TEMPORAL_ARTERY to MEASUREMENT_LOCATION_TEMPORAL_ARTERY,
+ TOE to MEASUREMENT_LOCATION_TOE,
+ EAR to MEASUREMENT_LOCATION_EAR,
+ WRIST to MEASUREMENT_LOCATION_WRIST,
+ VAGINA to MEASUREMENT_LOCATION_VAGINA
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_LOCATION_INT_TO_STRING_MAP = MEASUREMENT_LOCATION_STRING_TO_INT_MAP.reverse()
}
/**
@@ -37,19 +70,20 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
-@StringDef(
+@IntDef(
value =
[
- BodyTemperatureMeasurementLocation.ARMPIT,
- BodyTemperatureMeasurementLocation.FINGER,
- BodyTemperatureMeasurementLocation.FOREHEAD,
- BodyTemperatureMeasurementLocation.MOUTH,
- BodyTemperatureMeasurementLocation.RECTUM,
- BodyTemperatureMeasurementLocation.TEMPORAL_ARTERY,
- BodyTemperatureMeasurementLocation.TOE,
- BodyTemperatureMeasurementLocation.EAR,
- BodyTemperatureMeasurementLocation.WRIST,
- BodyTemperatureMeasurementLocation.VAGINA,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FINGER,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_FOREHEAD,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_MOUTH,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_RECTUM,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TEMPORAL_ARTERY,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_TOE,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_EAR,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_WRIST,
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_VAGINA
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
index 68abaf1..a939b11 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BodyTemperatureRecord.kt
@@ -35,7 +35,9 @@
*
* @see BodyTemperatureMeasurementLocation
*/
- @property:BodyTemperatureMeasurementLocations public val measurementLocation: String? = null,
+ @property:BodyTemperatureMeasurementLocations
+ public val measurementLocation: Int =
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
@@ -60,7 +62,7 @@
*/
override fun hashCode(): Int {
var result = temperature.hashCode()
- result = 31 * result + (measurementLocation?.hashCode() ?: 0)
+ result = 31 * result + measurementLocation
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
index c1cc1d0..95249de 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/CervicalMucusRecord.kt
@@ -19,8 +19,6 @@
import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.health.connect.client.records.CervicalMucusRecord.Appearances
-import androidx.health.connect.client.records.CervicalMucusRecord.Sensations
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -46,8 +44,10 @@
const val APPEARANCE_STICKY = 2
const val APPEARANCE_CREAMY = 3
const val APPEARANCE_WATERY = 4
+
/** A constant describing clear or egg white like looking cervical mucus. */
const val APPEARANCE_EGG_WHITE = 5
+
/** A constant describing an unusual (worth attention) kind of cervical mucus. */
const val APPEARANCE_UNUSUAL = 6
@@ -71,8 +71,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmField
- val APPEARANCE_INT_TO_STRING_MAP =
- APPEARANCE_STRING_TO_INT_MAP.entries.associate { it.value to it.key }
+ val APPEARANCE_INT_TO_STRING_MAP = APPEARANCE_STRING_TO_INT_MAP.reverse()
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmField
@@ -85,8 +84,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmField
- val SENSATION_INT_TO_STRING_MAP =
- SENSATION_STRING_TO_INT_MAP.entries.associate { it.value to it.key }
+ val SENSATION_INT_TO_STRING_MAP = SENSATION_STRING_TO_INT_MAP.reverse()
}
/** List of supported Cervical Mucus Sensation types on Health Platform. */
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
index 4cd7141..5e1809e 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
@@ -15,8 +15,8 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -37,7 +37,7 @@
*
* @see EventType
*/
- @property:EventTypes public val eventType: String,
+ @property:EventTypes public val eventType: Int,
override val metadata: Metadata = Metadata.EMPTY,
) : IntervalRecord {
@@ -60,8 +60,7 @@
}
override fun hashCode(): Int {
- var result = 0
- result = 31 * result + eventType.hashCode()
+ var result = eventType.hashCode()
result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
result = 31 * result + endTime.hashCode()
result = 31 * result + (endZoneOffset?.hashCode() ?: 0)
@@ -73,7 +72,7 @@
* Types of exercise event. They can be either explicitly requested by a user or auto-detected
* by a tracking app.
*/
- public object EventType {
+ internal object EventType {
/**
* Explicit pause during an workout, requested by the user (by clicking a pause button in
* the session UI). Movement happening during pause should not contribute to session
@@ -93,13 +92,32 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- EventType.PAUSE,
- EventType.REST,
+ EVENT_TYPE_UNKNOWN,
+ EVENT_TYPE_PAUSE,
+ EVENT_TYPE_REST,
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class EventTypes
+
+ companion object {
+ const val EVENT_TYPE_UNKNOWN = 0
+ const val EVENT_TYPE_PAUSE = 1
+ const val EVENT_TYPE_REST = 2
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val EVENT_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ EventType.PAUSE to EVENT_TYPE_PAUSE,
+ EventType.REST to EVENT_TYPE_REST,
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val EVENT_TYPE_INT_TO_STRING_MAP = EVENT_TYPE_STRING_TO_INT_MAP.reverse()
+ }
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
index 40299d1..3a6a40f 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MealType.kt
@@ -15,19 +15,49 @@
*/
package androidx.health.connect.client.records
-import androidx.annotation.StringDef
+import androidx.annotation.IntDef
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.MealType.MEAL_TYPE_BREAKFAST
+import androidx.health.connect.client.records.MealType.MEAL_TYPE_DINNER
+import androidx.health.connect.client.records.MealType.MEAL_TYPE_LUNCH
+import androidx.health.connect.client.records.MealType.MEAL_TYPE_SNACK
+import androidx.health.connect.client.records.MealType.MEAL_TYPE_UNKNOWN
/** Type of meal. */
public object MealType {
- const val UNKNOWN = "unknown"
+ internal const val UNKNOWN = "unknown"
+ internal const val BREAKFAST = "breakfast"
+ internal const val LUNCH = "lunch"
+ internal const val DINNER = "dinner"
+ internal const val SNACK = "snack"
+
+ const val MEAL_TYPE_UNKNOWN = 0
+
/** Use this for the first meal of the day, usually the morning meal. */
- const val BREAKFAST = "breakfast"
+ const val MEAL_TYPE_BREAKFAST = 1
+
/** Use this for the noon meal. */
- const val LUNCH = "lunch"
+ const val MEAL_TYPE_LUNCH = 2
+
/** Use this for last meal of the day, usually the evening meal. */
- const val DINNER = "dinner"
+ const val MEAL_TYPE_DINNER = 3
+
/** Any meal outside of the usual three meals per day. */
- const val SNACK = "snack"
+ const val MEAL_TYPE_SNACK = 4
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEAL_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ BREAKFAST to MEAL_TYPE_BREAKFAST,
+ LUNCH to MEAL_TYPE_LUNCH,
+ DINNER to MEAL_TYPE_DINNER,
+ SNACK to MEAL_TYPE_SNACK,
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEAL_TYPE_INT_TO_STRING_MAP = MEAL_TYPE_STRING_TO_INT_MAP.reverse()
}
/**
@@ -35,14 +65,8 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
-@StringDef(
+@IntDef(
value =
- [
- MealType.UNKNOWN,
- MealType.BREAKFAST,
- MealType.LUNCH,
- MealType.DINNER,
- MealType.SNACK,
- ]
+ [MEAL_TYPE_UNKNOWN, MEAL_TYPE_BREAKFAST, MEAL_TYPE_LUNCH, MEAL_TYPE_DINNER, MEAL_TYPE_SNACK]
)
annotation class MealTypes
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
index d2e3ab0..2148f53 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/NutritionRecord.kt
@@ -122,7 +122,7 @@
*
* @see MealType
*/
- @property:MealTypes public val mealType: String? = null,
+ @property:MealTypes public val mealType: Int = MealType.MEAL_TYPE_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : IntervalRecord {
@@ -237,7 +237,7 @@
result = 31 * result + vitaminK.hashCode()
result = 31 * result + zinc.hashCode()
result = 31 * result + (name?.hashCode() ?: 0)
- result = 31 * result + (mealType?.hashCode() ?: 0)
+ result = 31 * result + mealType
result = 31 * result + startTime.hashCode()
result = 31 * result + (startZoneOffset?.hashCode() ?: 0)
result = 31 * result + endTime.hashCode()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
index a7dd869..f5cf136 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/OvulationTestRecord.kt
@@ -15,8 +15,8 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -27,11 +27,9 @@
override val zoneOffset: ZoneOffset?,
/**
* The result of a user's ovulation test, which shows if they're ovulating or not. Required
- * field. Allowed values: [Result].
- *
- * @see Result
+ * field.
*/
- @property:Results public val result: String,
+ @property:Results public val result: Int,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
override fun equals(other: Any?): Boolean {
@@ -47,8 +45,7 @@
}
override fun hashCode(): Int {
- var result = 0
- result = 31 * result + result.hashCode()
+ var result = result.hashCode()
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
@@ -56,27 +53,52 @@
}
/** The result of a user's ovulation test. */
- object Result {
+ internal object Result {
+ const val POSITIVE = "positive"
+ const val HIGH = "high"
+ const val NEGATIVE = "negative"
+ const val INCONCLUSIVE = "inconclusive"
+ }
+
+ companion object {
+ /**
+ * Inconclusive result. Refers to ovulation test results that are indeterminate (e.g. may be
+ * testing malfunction, user error, etc.). ". Any unknown value will also be returned as
+ * [RESULT_INCONCLUSIVE].
+ */
+ const val RESULT_INCONCLUSIVE = 0
+
/**
* Positive fertility (may also be referred as "peak" fertility). Refers to the peak of the
* luteinizing hormone (LH) surge and ovulation is expected to occur in 10-36 hours.
*/
- const val POSITIVE = "positive"
+ const val RESULT_POSITIVE = 1
+
/**
* High fertility. Refers to a rise in estrogen or luteinizing hormone that may signal the
* fertile window (time in the menstrual cycle when conception is likely to occur).
*/
- const val HIGH = "high"
+ const val RESULT_HIGH = 2
+
/**
* Negative fertility (may also be referred as "low" fertility). Refers to the time in the
* cycle where fertility/conception is expected to be low.
*/
- const val NEGATIVE = "negative"
- /**
- * Inconclusive result. Refers to ovulation test results that are indeterminate (e.g. may be
- * testing malfunction, user error, etc.). "
- */
- const val INCONCLUSIVE = "inconclusive"
+ const val RESULT_NEGATIVE = 3
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val RESULT_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ Result.INCONCLUSIVE to RESULT_INCONCLUSIVE,
+ Result.POSITIVE to RESULT_POSITIVE,
+ Result.HIGH to RESULT_HIGH,
+ Result.NEGATIVE to RESULT_NEGATIVE
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val RESULT_INT_TO_STRING_MAP = RESULT_STRING_TO_INT_MAP.reverse()
}
/**
@@ -84,15 +106,7 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
- value =
- [
- Result.NEGATIVE,
- Result.POSITIVE,
- Result.INCONCLUSIVE,
- Result.HIGH,
- ]
- )
+ @IntDef(value = [RESULT_INCONCLUSIVE, RESULT_POSITIVE, RESULT_HIGH, RESULT_NEGATIVE])
@RestrictTo(RestrictTo.Scope.LIBRARY)
annotation class Results
}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/RelationToMeal.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/RelationToMeal.kt
deleted file mode 100644
index 6c7c04b..0000000
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/RelationToMeal.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.
- */
-package androidx.health.connect.client.records
-
-import androidx.annotation.StringDef
-
-/** Temporal relationship of measurement time to a meal. */
-public object RelationToMeal {
- const val GENERAL = "general"
- const val FASTING = "fasting"
- const val BEFORE_MEAL = "before_meal"
- const val AFTER_MEAL = "after_meal"
-}
-
-/**
- * Temporal relationship of measurement time to a meal.
- * @suppress
- */
-@Retention(AnnotationRetention.SOURCE)
-@StringDef(
- value =
- [
- RelationToMeal.GENERAL,
- RelationToMeal.FASTING,
- RelationToMeal.BEFORE_MEAL,
- RelationToMeal.AFTER_MEAL,
- ]
-)
-annotation class RelationToMeals
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
index ae0197c..9c5b578 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SexualActivityRecord.kt
@@ -15,8 +15,8 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -34,7 +34,7 @@
*
* @see Protection
*/
- @property:Protections public val protectionUsed: String? = null,
+ @property:Protections public val protectionUsed: Int = PROTECTION_USED_UNKNOWN,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
override fun equals(other: Any?): Boolean {
@@ -50,16 +50,34 @@
}
override fun hashCode(): Int {
- var result = 0
- result = 31 * result + protectionUsed.hashCode()
+ var result = protectionUsed
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
return result
}
+ companion object {
+ const val PROTECTION_USED_UNKNOWN = 0
+ const val PROTECTION_USED_PROTECTED = 1
+ const val PROTECTION_USED_UNPROTECTED = 2
+
+ /** Internal mappings useful for interoperability between integers and strings. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val PROTECTION_USED_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ Protection.PROTECTED to PROTECTION_USED_PROTECTED,
+ Protection.UNPROTECTED to PROTECTION_USED_UNPROTECTED,
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val PROTECTION_USED_INT_TO_STRING_MAP = PROTECTION_USED_STRING_TO_INT_MAP.reverse()
+ }
+
/** Whether protection was used during sexual activity. */
- public object Protection {
+ internal object Protection {
const val PROTECTED = "protected"
const val UNPROTECTED = "unprotected"
}
@@ -69,11 +87,11 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- SexualActivityRecord.Protection.PROTECTED,
- SexualActivityRecord.Protection.UNPROTECTED,
+ PROTECTION_USED_PROTECTED,
+ PROTECTION_USED_UNPROTECTED,
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
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/connect/client/records/SwimmingStrokesRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
index 96748aa..344eec9 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/SwimmingStrokesRecord.kt
@@ -15,8 +15,8 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -32,7 +32,7 @@
*
* @see SwimmingType
*/
- @property:SwimmingTypes public val type: String,
+ @property:SwimmingTypes public val type: Int,
/** Count of strokes. Optional field. Valid range: 1-1000000. */
public val count: Long = 0,
override val metadata: Metadata = Metadata.EMPTY,
@@ -68,8 +68,33 @@
return result
}
+ companion object {
+ const val SWIMMING_TYPE_OTHER = 0
+ const val SWIMMING_TYPE_FREESTYLE = 1
+ const val SWIMMING_TYPE_BACKSTROKE = 2
+ const val SWIMMING_TYPE_BREASTSTROKE = 3
+ const val SWIMMING_TYPE_BUTTERFLY = 4
+ const val SWIMMING_TYPE_MIXED = 5
+
+ /** Internal mappings useful for interoperability between integers and strings. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SWIMMING_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ SwimmingType.FREESTYLE to SWIMMING_TYPE_FREESTYLE,
+ SwimmingType.BACKSTROKE to SWIMMING_TYPE_BACKSTROKE,
+ SwimmingType.BREASTSTROKE to SWIMMING_TYPE_BREASTSTROKE,
+ SwimmingType.BUTTERFLY to SWIMMING_TYPE_BUTTERFLY,
+ SwimmingType.MIXED to SWIMMING_TYPE_MIXED,
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val SWIMMING_TYPE_INT_TO_STRING_MAP = SWIMMING_TYPE_STRING_TO_INT_MAP.reverse()
+ }
+
/** List of Swimming styles. */
- public object SwimmingType {
+ internal object SwimmingType {
const val FREESTYLE = "freestyle"
const val BACKSTROKE = "backstroke"
const val BREASTSTROKE = "breaststroke"
@@ -82,15 +107,15 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- SwimmingType.FREESTYLE,
- SwimmingType.BACKSTROKE,
- SwimmingType.BREASTSTROKE,
- SwimmingType.BUTTERFLY,
- SwimmingType.MIXED,
- SwimmingType.OTHER,
+ SWIMMING_TYPE_FREESTYLE,
+ SWIMMING_TYPE_BACKSTROKE,
+ SWIMMING_TYPE_BREASTSTROKE,
+ SWIMMING_TYPE_BUTTERFLY,
+ SWIMMING_TYPE_MIXED,
+ SWIMMING_TYPE_OTHER,
]
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
index ecd101b..a6cb11b 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Utils.kt
@@ -35,3 +35,7 @@
internal fun requireNonNegative(value: Double, name: String) {
require(value >= 0.0) { "$name must not be negative" }
}
+
+internal fun Map<String, Int>.reverse(): Map<Int, String> {
+ return entries.associateBy({ it.value }, { it.key })
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
index 0f0942b..0a6f4df 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/Vo2MaxRecord.kt
@@ -15,8 +15,8 @@
*/
package androidx.health.connect.client.records
+import androidx.annotation.IntDef
import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
import androidx.health.connect.client.records.metadata.Metadata
import java.time.Instant
import java.time.ZoneOffset
@@ -32,7 +32,7 @@
*
* @see MeasurementMethod
*/
- @property:MeasurementMethods public val measurementMethod: String? = null,
+ @property:MeasurementMethods public val measurementMethod: Int = MEASUREMENT_METHOD_OTHER,
override val metadata: Metadata = Metadata.EMPTY,
) : InstantaneousRecord {
init {
@@ -61,15 +61,41 @@
override fun hashCode(): Int {
var result = 0
result = 31 * result + vo2MillilitersPerMinuteKilogram.hashCode()
- result = 31 * result + measurementMethod.hashCode()
+ result = 31 * result + measurementMethod
result = 31 * result + time.hashCode()
result = 31 * result + (zoneOffset?.hashCode() ?: 0)
result = 31 * result + metadata.hashCode()
return result
}
+ companion object {
+ const val MEASUREMENT_METHOD_OTHER = 0
+ const val MEASUREMENT_METHOD_METABOLIC_CART = 1
+ const val MEASUREMENT_METHOD_HEART_RATE_RATIO = 2
+ const val MEASUREMENT_METHOD_COOPER_TEST = 3
+ const val MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST = 4
+ const val MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST = 5
+
+ /** Internal mappings useful for interoperability between integers and strings. */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_METHOD_STRING_TO_INT_MAP: Map<String, Int> =
+ mapOf(
+ MeasurementMethod.METABOLIC_CART to MEASUREMENT_METHOD_METABOLIC_CART,
+ MeasurementMethod.HEART_RATE_RATIO to MEASUREMENT_METHOD_HEART_RATE_RATIO,
+ MeasurementMethod.COOPER_TEST to MEASUREMENT_METHOD_COOPER_TEST,
+ MeasurementMethod.MULTISTAGE_FITNESS_TEST to
+ MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
+ MeasurementMethod.ROCKPORT_FITNESS_TEST to MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST,
+ )
+
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @JvmField
+ val MEASUREMENT_METHOD_INT_TO_STRING_MAP = MEASUREMENT_METHOD_STRING_TO_INT_MAP.reverse()
+ }
+
/** VO2 max (maximal aerobic capacity) measurement method. */
- public object MeasurementMethod {
+ internal object MeasurementMethod {
const val METABOLIC_CART = "metabolic_cart"
const val HEART_RATE_RATIO = "heart_rate_ratio"
const val COOPER_TEST = "cooper_test"
@@ -83,15 +109,15 @@
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
- @StringDef(
+ @IntDef(
value =
[
- MeasurementMethod.METABOLIC_CART,
- MeasurementMethod.HEART_RATE_RATIO,
- MeasurementMethod.COOPER_TEST,
- MeasurementMethod.MULTISTAGE_FITNESS_TEST,
- MeasurementMethod.ROCKPORT_FITNESS_TEST,
- MeasurementMethod.OTHER,
+ MEASUREMENT_METHOD_OTHER,
+ MEASUREMENT_METHOD_METABOLIC_CART,
+ MEASUREMENT_METHOD_HEART_RATE_RATIO,
+ MEASUREMENT_METHOD_COOPER_TEST,
+ MEASUREMENT_METHOD_MULTISTAGE_FITNESS_TEST,
+ MEASUREMENT_METHOD_ROCKPORT_FITNESS_TEST,
]
)
@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/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 05d3e87..0291436 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -31,7 +31,6 @@
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.ExerciseEventRecord.EventType
import androidx.health.connect.client.records.ExerciseLapRecord
import androidx.health.connect.client.records.ExerciseRepetitionsRecord
import androidx.health.connect.client.records.ExerciseRepetitionsRecord.Companion.REPETITION_TYPE_JUMPING_JACK
@@ -53,10 +52,10 @@
import androidx.health.connect.client.records.HydrationRecord
import androidx.health.connect.client.records.IntermenstrualBleedingRecord
import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MealType
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.OvulationTestRecord.Result
import androidx.health.connect.client.records.OxygenSaturationRecord
import androidx.health.connect.client.records.PowerRecord
import androidx.health.connect.client.records.Record
@@ -70,7 +69,7 @@
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.SwimmingStrokesRecord.SwimmingType
+import androidx.health.connect.client.records.SwimmingStrokesRecord.Companion.SWIMMING_TYPE_BACKSTROKE
import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
import androidx.health.connect.client.records.Vo2MaxRecord
import androidx.health.connect.client.records.WaistCircumferenceRecord
@@ -124,7 +123,6 @@
val dataOnlyRequired =
BasalBodyTemperatureRecord(
temperature = 1.celsius,
- measurementLocation = null,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -133,7 +131,8 @@
val dataAllFields =
BasalBodyTemperatureRecord(
temperature = 1.celsius,
- measurementLocation = BodyTemperatureMeasurementLocation.ARMPIT,
+ measurementLocation =
+ BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_ARMPIT,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -163,9 +162,6 @@
val data =
BloodGlucoseRecord(
level = BloodGlucose.millimolesPerLiter(1.0),
- specimenSource = null,
- mealType = null,
- relationToMeal = null,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -181,8 +177,6 @@
BloodPressureRecord(
systolic = 20.millimetersOfMercury,
diastolic = 10.millimetersOfMercury,
- bodyPosition = null,
- measurementLocation = null,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -211,7 +205,6 @@
val data =
BodyTemperatureRecord(
temperature = 1.celsius,
- measurementLocation = null,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -519,7 +512,7 @@
fun testOvulationTest() {
val data =
OvulationTestRecord(
- result = Result.NEGATIVE,
+ result = OvulationTestRecord.RESULT_NEGATIVE,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -601,7 +594,7 @@
fun testSexualActivity() {
val data =
SexualActivityRecord(
- protectionUsed = null,
+ protectionUsed = SexualActivityRecord.PROTECTION_USED_PROTECTED,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -668,7 +661,7 @@
val data =
Vo2MaxRecord(
vo2MillilitersPerMinuteKilogram = 1.0,
- measurementMethod = null,
+ measurementMethod = Vo2MaxRecord.MEASUREMENT_METHOD_COOPER_TEST,
time = START_TIME,
zoneOffset = END_ZONE_OFFSET,
metadata = TEST_METADATA
@@ -726,7 +719,7 @@
fun testActivityEvent() {
val data =
ExerciseEventRecord(
- eventType = EventType.PAUSE,
+ eventType = ExerciseEventRecord.EVENT_TYPE_REST,
startTime = START_TIME,
startZoneOffset = START_ZONE_OFFSET,
endTime = END_TIME,
@@ -882,7 +875,7 @@
vitaminE = 1.grams,
vitaminK = 1.grams,
zinc = 1.grams,
- mealType = null,
+ mealType = MealType.MEAL_TYPE_BREAKFAST,
name = null,
startTime = START_TIME,
startZoneOffset = START_ZONE_OFFSET,
@@ -966,7 +959,7 @@
val data =
SwimmingStrokesRecord(
count = 1,
- type = SwimmingType.BACKSTROKE,
+ type = SWIMMING_TYPE_BACKSTROKE,
startTime = START_TIME,
startZoneOffset = START_ZONE_OFFSET,
endTime = END_TIME,
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/BloodGlucoseRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BloodGlucoseRecordTest.kt
new file mode 100644
index 0000000..590b879
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BloodGlucoseRecordTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BloodGlucoseRecordTest {
+
+ @Test
+ fun relationToMealEnums_existInMapping() {
+ val allEnums =
+ BloodGlucoseRecord.Companion::class.allIntDefEnumsWithPrefix("RELATION_TO_MEAL")
+
+ assertThat(BloodGlucoseRecord.RELATION_TO_MEAL_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(BloodGlucoseRecord.RELATION_TO_MEAL_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+
+ @Test
+ fun specimenSourceEnums_existInMapping() {
+ val allEnums =
+ BloodGlucoseRecord.Companion::class.allIntDefEnumsWithPrefix("SPECIMEN_SOURCE")
+
+ assertThat(BloodGlucoseRecord.SPECIMEN_SOURCE_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(BloodGlucoseRecord.SPECIMEN_SOURCE_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BloodPressureRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BloodPressureRecordTest.kt
new file mode 100644
index 0000000..63539bd1
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BloodPressureRecordTest.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BloodPressureRecordTest {
+ @Test
+ fun bodyPositionEnums_existInMapping() {
+ val allEnums =
+ BloodPressureRecord.Companion::class.allIntDefEnumsWithPrefix("BODY_POSITION")
+
+ assertThat(BloodPressureRecord.BODY_POSITION_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(BloodPressureRecord.BODY_POSITION_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+
+ @Test
+ fun measurementLocationEnums_existInMapping() {
+ val allEnums =
+ BloodPressureRecord.Companion::class.allIntDefEnumsWithPrefix("MEASUREMENT_LOCATION")
+
+ assertThat(BloodPressureRecord.MEASUREMENT_LOCATION_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(BloodPressureRecord.MEASUREMENT_LOCATION_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocationTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocationTest.kt
new file mode 100644
index 0000000..0a805f4
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/BodyTemperatureMeasurementLocationTest.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.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class BodyTemperatureMeasurementLocationTest {
+
+ @Test
+ fun enums_existInMapping() {
+ val allEnums =
+ BodyTemperatureMeasurementLocation::class.allObjectIntDefEnumsWithPrefix(
+ "MEASUREMENT_LOCATION"
+ )
+
+ assertThat(BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(BodyTemperatureMeasurementLocation.MEASUREMENT_LOCATION_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/CervicalMucusRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/CervicalMucusRecordTest.kt
new file mode 100644
index 0000000..ad92237
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/CervicalMucusRecordTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CervicalMucusRecordTest {
+
+ @Test
+ fun appearanceEnums_existInMapping() {
+ val allEnums = CervicalMucusRecord.Companion::class.allIntDefEnumsWithPrefix("APPEARANCE")
+
+ assertThat(CervicalMucusRecord.APPEARANCE_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(CervicalMucusRecord.APPEARANCE_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+
+ @Test
+ fun sensationEnums_existInMapping() {
+ val allEnums = CervicalMucusRecord.Companion::class.allIntDefEnumsWithPrefix("SENSATION")
+
+ assertThat(CervicalMucusRecord.SENSATION_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(CervicalMucusRecord.SENSATION_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/EnumTestUtils.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/EnumTestUtils.kt
new file mode 100644
index 0000000..ddaf765
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/EnumTestUtils.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.
+ */
+
+package androidx.health.connect.client.records
+
+import kotlin.reflect.KClass
+import kotlin.reflect.typeOf
+
+internal fun KClass<*>.allIntDefEnumsWithPrefix(prefix: String): Collection<Int> {
+ return members
+ .asSequence()
+ .filter { it.name.startsWith(prefix) && !it.name.endsWith("UNKNOWN") }
+ .filter { it.returnType == typeOf<Int>() }
+ .map { it.call(null) }
+ .filterIsInstance<Int>()
+ .toHashSet()
+}
+
+internal fun KClass<*>.allObjectIntDefEnumsWithPrefix(prefix: String): Collection<Int> {
+ return members
+ .asSequence()
+ .filter { it.name.startsWith(prefix) && !it.name.endsWith("UNKNOWN") }
+ .filter { it.returnType == typeOf<Int>() }
+ .map { it.call() }
+ .filterIsInstance<Int>()
+ .toHashSet()
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt
index 9ad7be7..022994e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseEventRecordTest.kt
@@ -34,7 +34,7 @@
startZoneOffset = null,
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
- eventType = ExerciseEventRecord.EventType.PAUSE,
+ eventType = ExerciseEventRecord.EVENT_TYPE_REST,
)
)
.isEqualTo(
@@ -43,7 +43,7 @@
startZoneOffset = null,
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
- eventType = ExerciseEventRecord.EventType.PAUSE,
+ eventType = ExerciseEventRecord.EVENT_TYPE_REST,
)
)
}
@@ -56,8 +56,18 @@
startZoneOffset = null,
endTime = Instant.ofEpochMilli(1234L),
endZoneOffset = null,
- eventType = ExerciseEventRecord.EventType.PAUSE,
+ eventType = ExerciseEventRecord.EVENT_TYPE_REST,
)
}
}
+
+ @Test
+ fun eventEnums_existInMapping() {
+ val allEnums = ExerciseEventRecord.Companion::class.allIntDefEnumsWithPrefix("EVENT_TYPE")
+
+ assertThat(ExerciseEventRecord.EVENT_TYPE_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(ExerciseEventRecord.EVENT_TYPE_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/FloorsClimbedRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/FloorsClimbedRecordTest.kt
index ba9940c..4077757 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/FloorsClimbedRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/FloorsClimbedRecordTest.kt
@@ -17,7 +17,7 @@
package androidx.health.connect.client.records
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
import java.time.Instant
import kotlin.test.assertFailsWith
import org.junit.Test
@@ -28,7 +28,7 @@
@Test
fun validRecord_equals() {
- Truth.assertThat(
+ assertThat(
FloorsClimbedRecord(
startTime = Instant.ofEpochMilli(1234L),
startZoneOffset = null,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/MealTypeTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/MealTypeTest.kt
new file mode 100644
index 0000000..de588ab
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/MealTypeTest.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.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class MealTypeTest {
+
+ @Test
+ fun enums_existInMapping() {
+ val allEnums = MealType::class.allObjectIntDefEnumsWithPrefix("MEAL_TYPE")
+
+ assertThat(MealType.MEAL_TYPE_STRING_TO_INT_MAP.values).containsExactlyElementsIn(allEnums)
+ assertThat(MealType.MEAL_TYPE_INT_TO_STRING_MAP.keys).containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/OvulationTestRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/OvulationTestRecordTest.kt
new file mode 100644
index 0000000..d3f74d9
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/OvulationTestRecordTest.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.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class OvulationTestRecordTest {
+
+ @Test
+ fun resultEnums_existInMapping() {
+ val allEnums = OvulationTestRecord.Companion::class.allIntDefEnumsWithPrefix("RESULT")
+
+ assertThat(OvulationTestRecord.RESULT_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(OvulationTestRecord.RESULT_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SexualActivityRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SexualActivityRecordTest.kt
new file mode 100644
index 0000000..e829d17
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SexualActivityRecordTest.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.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SexualActivityRecordTest {
+
+ @Test
+ fun protectionEnums_existInMapping() {
+ val allEnums =
+ SexualActivityRecord.Companion::class.allIntDefEnumsWithPrefix("PROTECTION_USED")
+
+ Truth.assertThat(SexualActivityRecord.PROTECTION_USED_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ Truth.assertThat(SexualActivityRecord.PROTECTION_USED_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
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/connect/client/records/SwimmingStrokesRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt
index ed30cca..8428f38 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/SwimmingStrokesRecordTest.kt
@@ -35,7 +35,7 @@
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
count = 10,
- type = SwimmingStrokesRecord.SwimmingType.BACKSTROKE,
+ type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
)
)
.isEqualTo(
@@ -45,7 +45,7 @@
endTime = Instant.ofEpochMilli(1236L),
endZoneOffset = null,
count = 10,
- type = SwimmingStrokesRecord.SwimmingType.BACKSTROKE,
+ type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
)
)
}
@@ -59,8 +59,20 @@
endTime = Instant.ofEpochMilli(1234L),
endZoneOffset = null,
count = 10,
- type = SwimmingStrokesRecord.SwimmingType.BACKSTROKE,
+ type = SwimmingStrokesRecord.SWIMMING_TYPE_BACKSTROKE,
)
}
}
+
+ @Test
+ fun eventEnums_existInMapping() {
+ val allEnums =
+ SwimmingStrokesRecord.Companion::class.allIntDefEnumsWithPrefix("SWIMMING_TYPE")
+ .filter { it != SwimmingStrokesRecord.SWIMMING_TYPE_OTHER }
+
+ assertThat(SwimmingStrokesRecord.SWIMMING_TYPE_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ assertThat(SwimmingStrokesRecord.SWIMMING_TYPE_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/Vo2MaxRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/Vo2MaxRecordTest.kt
new file mode 100644
index 0000000..a414d8c
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/Vo2MaxRecordTest.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class Vo2MaxRecordTest {
+ @Test
+ fun measurementMethodEnums_existMapping() {
+ val allEnums =
+ Vo2MaxRecord.Companion::class.allIntDefEnumsWithPrefix("MEASUREMENT_METHOD").filter {
+ it != Vo2MaxRecord.MEASUREMENT_METHOD_OTHER
+ }
+
+ Truth.assertThat(Vo2MaxRecord.MEASUREMENT_METHOD_STRING_TO_INT_MAP.values)
+ .containsExactlyElementsIn(allEnums)
+ Truth.assertThat(Vo2MaxRecord.MEASUREMENT_METHOD_INT_TO_STRING_MAP.keys)
+ .containsExactlyElementsIn(allEnums)
+ }
+}
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(